mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 08:23:15 +01:00
fix: only skip tmpfs mounts for some paths (#2918)
* fix: only skip tmpfs mounts for some paths Signed-off-by: Will Murphy <will.murphy@anchore.com> * refactor and add tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * add regression test for archive processing Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * bump to golang 1.22 Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * remove rule 1 and add more tests Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Will Murphy <will.murphy@anchore.com> Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
parent
cb09dd9e19
commit
557ad73ee6
2
.github/actions/bootstrap/action.yaml
vendored
2
.github/actions/bootstrap/action.yaml
vendored
@ -5,7 +5,7 @@ inputs:
|
|||||||
go-version:
|
go-version:
|
||||||
description: "Go version to install"
|
description: "Go version to install"
|
||||||
required: true
|
required: true
|
||||||
default: "1.21.x"
|
default: "1.22.x"
|
||||||
go-dependencies:
|
go-dependencies:
|
||||||
description: "Download go dependencies"
|
description: "Download go dependencies"
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
|||||||
module github.com/anchore/syft
|
module github.com/anchore/syft
|
||||||
|
|
||||||
go 1.21.0
|
go 1.22.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/CycloneDX/cyclonedx-go v0.8.0
|
github.com/CycloneDX/cyclonedx-go v0.8.0
|
||||||
|
|||||||
@ -9,13 +9,11 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/moby/sys/mountinfo"
|
|
||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
"github.com/wagoodman/go-progress"
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
"github.com/anchore/stereoscope/pkg/file"
|
"github.com/anchore/stereoscope/pkg/file"
|
||||||
"github.com/anchore/stereoscope/pkg/filetree"
|
"github.com/anchore/stereoscope/pkg/filetree"
|
||||||
"github.com/anchore/syft/internal"
|
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/event"
|
"github.com/anchore/syft/syft/event"
|
||||||
@ -43,7 +41,8 @@ func newDirectoryIndexer(path, base string, visitors ...PathIndexVisitor) *direc
|
|||||||
[]PathIndexVisitor{
|
[]PathIndexVisitor{
|
||||||
requireFileInfo,
|
requireFileInfo,
|
||||||
disallowByFileType,
|
disallowByFileType,
|
||||||
newUnixSystemMountFinder().disallowUnixSystemRuntimePath},
|
skipPathsByMountTypeAndName(path),
|
||||||
|
},
|
||||||
visitors...,
|
visitors...,
|
||||||
),
|
),
|
||||||
errPaths: make(map[string]error),
|
errPaths: make(map[string]error),
|
||||||
@ -450,57 +449,6 @@ func (r *directoryIndexer) disallowRevisitingVisitor(_, path string, _ os.FileIn
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type unixSystemMountFinder struct {
|
|
||||||
disallowedMountPaths []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUnixSystemMountFinder() unixSystemMountFinder {
|
|
||||||
infos, err := mountinfo.GetMounts(nil)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields("error", err).Warnf("unable to get system mounts")
|
|
||||||
return unixSystemMountFinder{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return unixSystemMountFinder{
|
|
||||||
disallowedMountPaths: keepUnixSystemMountPaths(infos),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func keepUnixSystemMountPaths(infos []*mountinfo.Info) []string {
|
|
||||||
var mountPaths []string
|
|
||||||
for _, info := range infos {
|
|
||||||
if info == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// we're only interested in ignoring the logical filesystems typically found at these mount points:
|
|
||||||
// - /proc
|
|
||||||
// - procfs
|
|
||||||
// - proc
|
|
||||||
// - /sys
|
|
||||||
// - sysfs
|
|
||||||
// - /dev
|
|
||||||
// - devfs - BSD/darwin flavored systems and old linux systems
|
|
||||||
// - devtmpfs - driver core maintained /dev tmpfs
|
|
||||||
// - udev - userspace implementation that replaced devfs
|
|
||||||
// - tmpfs - used for /dev in special instances (within a container)
|
|
||||||
|
|
||||||
switch info.FSType {
|
|
||||||
case "proc", "procfs", "sysfs", "devfs", "devtmpfs", "udev", "tmpfs":
|
|
||||||
log.WithFields("mountpoint", info.Mountpoint).Debug("ignoring system mountpoint")
|
|
||||||
|
|
||||||
mountPaths = append(mountPaths, info.Mountpoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mountPaths
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f unixSystemMountFinder) disallowUnixSystemRuntimePath(_, path string, _ os.FileInfo, _ error) error {
|
|
||||||
if internal.HasAnyOfPrefixes(path, f.disallowedMountPaths...) {
|
|
||||||
return fs.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func disallowByFileType(_, _ string, info os.FileInfo, _ error) error {
|
func disallowByFileType(_, _ string, info os.FileInfo, _ error) error {
|
||||||
if info == nil {
|
if info == nil {
|
||||||
// we can't filter out by filetype for non-existent files
|
// we can't filter out by filetype for non-existent files
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/moby/sys/mountinfo"
|
|
||||||
"github.com/scylladb/go-set/strset"
|
"github.com/scylladb/go-set/strset"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -462,145 +461,3 @@ func relativePath(basePath, givenPath string) string {
|
|||||||
|
|
||||||
return relPath
|
return relPath
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_disallowUnixSystemRuntimePath(t *testing.T) {
|
|
||||||
unixSubject := unixSystemMountFinder{
|
|
||||||
// mock out detecting the mount points
|
|
||||||
disallowedMountPaths: []string{"/proc", "/sys", "/dev"},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
base string
|
|
||||||
expected error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "relative path to proc is allowed",
|
|
||||||
path: "proc/place",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "relative path within proc is not allowed",
|
|
||||||
path: "/proc/place",
|
|
||||||
expected: fs.SkipDir,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "path exactly to proc is not allowed",
|
|
||||||
path: "/proc",
|
|
||||||
expected: fs.SkipDir,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "similar to proc",
|
|
||||||
path: "/pro/c",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "similar to proc",
|
|
||||||
path: "/pro",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "dev is not allowed",
|
|
||||||
path: "/dev",
|
|
||||||
expected: fs.SkipDir,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sys is not allowed",
|
|
||||||
path: "/sys",
|
|
||||||
expected: fs.SkipDir,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unrelated allowed path",
|
|
||||||
path: "/something/sys",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "do not consider base when matching paths (non-matching)",
|
|
||||||
base: "/a/b/c",
|
|
||||||
path: "/a/b/c/dev",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "do not consider base when matching paths (matching)",
|
|
||||||
base: "/a/b/c",
|
|
||||||
path: "/dev",
|
|
||||||
expected: fs.SkipDir,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.path, func(t *testing.T) {
|
|
||||||
assert.Equal(t, test.expected, unixSubject.disallowUnixSystemRuntimePath(test.base, test.path, nil, nil))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_keepUnixSystemMountPaths(t *testing.T) {
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
infos []*mountinfo.Info
|
|
||||||
want []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "all valid filesystems",
|
|
||||||
infos: []*mountinfo.Info{
|
|
||||||
{
|
|
||||||
Mountpoint: "/etc/hostname",
|
|
||||||
FSType: "/dev/vda1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Mountpoint: "/sys/fs/cgroup",
|
|
||||||
FSType: "cgroup",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Mountpoint: "/",
|
|
||||||
FSType: "overlay",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: nil,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "no valid filesystems",
|
|
||||||
infos: []*mountinfo.Info{
|
|
||||||
{
|
|
||||||
Mountpoint: "/proc",
|
|
||||||
FSType: "proc",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Mountpoint: "/proc-2",
|
|
||||||
FSType: "procfs",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Mountpoint: "/sys",
|
|
||||||
FSType: "sysfs",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Mountpoint: "/dev",
|
|
||||||
FSType: "devfs",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Mountpoint: "/dev-u",
|
|
||||||
FSType: "udev",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Mountpoint: "/dev-tmp",
|
|
||||||
FSType: "devtmpfs",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Mountpoint: "/run",
|
|
||||||
FSType: "tmpfs",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
want: []string{
|
|
||||||
"/proc",
|
|
||||||
"/proc-2",
|
|
||||||
"/sys",
|
|
||||||
"/dev",
|
|
||||||
"/dev-u",
|
|
||||||
"/dev-tmp",
|
|
||||||
"/run",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
assert.Equal(t, tt.want, keepUnixSystemMountPaths(tt.infos))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
157
syft/internal/fileresolver/path_skipper.go
Normal file
157
syft/internal/fileresolver/path_skipper.go
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
package fileresolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/moby/sys/mountinfo"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type pathSkipper struct {
|
||||||
|
// scanTarget is the root path that is being scanned (without any base-path logic applied).
|
||||||
|
scanTarget string
|
||||||
|
|
||||||
|
// ignorableMountTypes is a set of mount types that should be ignored. Optionally a list of paths (the map values)
|
||||||
|
// can be provided that this mount type should be ignored at. For example in some containers /dev is mounted
|
||||||
|
// as a tmpfs and should be ignored, but /tmp should not be ignored. An empty list of paths means that paths
|
||||||
|
// within the mount type should always be ignored.
|
||||||
|
ignorableMountTypes map[string][]string
|
||||||
|
|
||||||
|
// current mount paths for the current system
|
||||||
|
mounts []*mountinfo.Info
|
||||||
|
mountsByType map[string][]*mountinfo.Info
|
||||||
|
}
|
||||||
|
|
||||||
|
// skipPathsByMountTypeAndName accepts the root path and returns a PathIndexVisitor that will skip paths based
|
||||||
|
// the filesystem type, the mountpoint, and configured blocklist paths for each filesystem type.
|
||||||
|
// This will help syft dodge filesystem topologies that have the potential to make the search space much bigger in
|
||||||
|
// areas known to not traditionally contain files of interest (installed software). It is meant to allow scanning
|
||||||
|
// "/" on a unix host to succeed, while also not causing any files in a narrow directory scan to be skipped unnecessarily.
|
||||||
|
func skipPathsByMountTypeAndName(root string) PathIndexVisitor {
|
||||||
|
infos, err := mountinfo.GetMounts(nil)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields("error", err).Warnf("unable to get system mounts")
|
||||||
|
return func(_ string, _ string, _ os.FileInfo, _ error) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPathSkipperFromMounts(root, infos).pathIndexVisitor
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPathSkipperFromMounts(root string, infos []*mountinfo.Info) pathSkipper {
|
||||||
|
// we're only interested in ignoring the logical filesystems typically found at these mount points:
|
||||||
|
// - /proc
|
||||||
|
// - procfs
|
||||||
|
// - proc
|
||||||
|
// - /sys
|
||||||
|
// - sysfs
|
||||||
|
// - /dev
|
||||||
|
// - devfs - BSD/darwin flavored systems and old linux systems
|
||||||
|
// - devtmpfs - driver core maintained /dev tmpfs
|
||||||
|
// - udev - userspace implementation that replaced devfs
|
||||||
|
// - tmpfs - used for /dev in special instances (within a container)
|
||||||
|
ignorableMountTypes := map[string][]string{
|
||||||
|
"proc": nil,
|
||||||
|
"procfs": nil,
|
||||||
|
"sysfs": nil,
|
||||||
|
"devfs": nil,
|
||||||
|
"devtmpfs": nil,
|
||||||
|
"udev": nil,
|
||||||
|
// note: there should be no order required (e.g. search /sys/thing before /sys) since that would imply that
|
||||||
|
// we could not ignore a nested path within a path that would be ignored anyway.
|
||||||
|
"tmpfs": {"/run", "/dev", "/var/run", "/var/lock", "/sys"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// The longest path is the most specific path, e.g.
|
||||||
|
// if / is mounted as tmpfs, but /home/syft/permanent is mounted as ext4,
|
||||||
|
// then the mount type for /home/syft/permanent/foo is ext4, and the mount info
|
||||||
|
// stating that /home/syft/permanent is ext4 has the longer mount point.
|
||||||
|
sort.Slice(infos, func(i, j int) bool {
|
||||||
|
return len(infos[i].Mountpoint) > len(infos[j].Mountpoint)
|
||||||
|
})
|
||||||
|
|
||||||
|
mountsByType := make(map[string][]*mountinfo.Info)
|
||||||
|
|
||||||
|
for _, mi := range infos {
|
||||||
|
mountsByType[mi.FSType] = append(mountsByType[mi.FSType], mi)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathSkipper{
|
||||||
|
scanTarget: root,
|
||||||
|
ignorableMountTypes: ignorableMountTypes,
|
||||||
|
mounts: infos,
|
||||||
|
mountsByType: mountsByType,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ps pathSkipper) pathIndexVisitor(_ string, givenPath string, _ os.FileInfo, _ error) error {
|
||||||
|
for _, mi := range ps.mounts {
|
||||||
|
conditionalPaths, ignorable := ps.ignorableMountTypes[mi.FSType]
|
||||||
|
|
||||||
|
if len(conditionalPaths) == 0 {
|
||||||
|
// Rule 1: ignore any path within a mount point that is of the given filesystem type unconditionally
|
||||||
|
if !containsPath(givenPath, mi.Mountpoint) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ignorable {
|
||||||
|
// we've matched on the most specific path at this point, which means we should stop searching
|
||||||
|
// mount points for this path
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(
|
||||||
|
"path", givenPath,
|
||||||
|
"mountpoint", mi.Mountpoint,
|
||||||
|
"fs", mi.FSType,
|
||||||
|
).Debug("ignoring path based on mountpoint filesystem type")
|
||||||
|
|
||||||
|
return fs.SkipDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rule 2: ignore any path within a mount point that is of the given filesystem type, only if
|
||||||
|
// the path is on a known blocklist of paths for that filesystem type.
|
||||||
|
// For example: /dev can be mounted as a tmpfs, which should always be skipped.
|
||||||
|
for _, conditionalPath := range conditionalPaths {
|
||||||
|
if !containsPath(givenPath, conditionalPath) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.WithFields(
|
||||||
|
"path", givenPath,
|
||||||
|
"mountpoint", mi.Mountpoint,
|
||||||
|
"fs", mi.FSType,
|
||||||
|
"condition", conditionalPath,
|
||||||
|
).Debug("ignoring path based on mountpoint filesystem type")
|
||||||
|
|
||||||
|
return fs.SkipDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsPath(p1, p2 string) bool {
|
||||||
|
p1Clean := simpleClean(p1)
|
||||||
|
p2Clean := simpleClean(p2)
|
||||||
|
if p1Clean == p2Clean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return strings.HasPrefix(p1Clean, p2Clean+"/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func simpleClean(p string) string {
|
||||||
|
p = strings.TrimSpace(p)
|
||||||
|
if p == "" {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
if p == "/" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
return strings.TrimSuffix(p, "/")
|
||||||
|
}
|
||||||
395
syft/internal/fileresolver/path_skipper_test.go
Normal file
395
syft/internal/fileresolver/path_skipper_test.go
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
package fileresolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/moby/sys/mountinfo"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_newPathSkipper(t *testing.T) {
|
||||||
|
type expect struct {
|
||||||
|
path string
|
||||||
|
wantErr assert.ErrorAssertionFunc
|
||||||
|
}
|
||||||
|
unixSubject := []*mountinfo.Info{
|
||||||
|
{
|
||||||
|
Mountpoint: "/proc",
|
||||||
|
FSType: "procfs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/sys",
|
||||||
|
FSType: "sysfs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/dev",
|
||||||
|
FSType: "devfs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/",
|
||||||
|
FSType: "/dev/disk3s1s1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/dev/shm",
|
||||||
|
FSType: "shm",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/tmp",
|
||||||
|
FSType: "tmpfs",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
root string
|
||||||
|
base string
|
||||||
|
mounts []*mountinfo.Info
|
||||||
|
want []expect
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "happy path",
|
||||||
|
root: "/somewhere",
|
||||||
|
mounts: []*mountinfo.Info{
|
||||||
|
{
|
||||||
|
Mountpoint: "/home/somewhere/else",
|
||||||
|
FSType: "/dev/disk3s6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/somewhere",
|
||||||
|
FSType: "/dev/disk3s7",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []expect{
|
||||||
|
{
|
||||||
|
// within a known mountpoint with valid type (1)
|
||||||
|
path: "/somewhere/dev",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// is a known mountpoint with valid type
|
||||||
|
path: "/somewhere",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// within a known mountpoint with valid type (2)
|
||||||
|
path: "/home/somewhere/else/too",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// outside of any known mountpoint should not be an error
|
||||||
|
path: "/bogus",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignore paths within a scan target",
|
||||||
|
root: "/somewhere",
|
||||||
|
mounts: []*mountinfo.Info{
|
||||||
|
{
|
||||||
|
Mountpoint: "/somewhere/doesnt/matter/proc",
|
||||||
|
FSType: "procfs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/somewhere",
|
||||||
|
FSType: "/dev/disk3s7",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []expect{
|
||||||
|
{
|
||||||
|
// within a known mountpoint with valid type (1)
|
||||||
|
path: "/somewhere/dev",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// is a known mountpoint with valid type
|
||||||
|
path: "/somewhere",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// mountpoint that should be ignored
|
||||||
|
path: "/somewhere/doesnt/matter/proc",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// within a mountpoint that should be ignored
|
||||||
|
path: "/somewhere/doesnt/matter/proc",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nested mountpoints behave correctly",
|
||||||
|
root: "/somewhere",
|
||||||
|
mounts: []*mountinfo.Info{
|
||||||
|
{
|
||||||
|
Mountpoint: "/somewhere/dev",
|
||||||
|
FSType: "devfs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/somewhere/dev/includeme",
|
||||||
|
FSType: "/dev/disk3s7",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []expect{
|
||||||
|
{
|
||||||
|
// is a known mountpoint with valid type
|
||||||
|
path: "/somewhere/dev",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// is a known mountpoint with valid type
|
||||||
|
path: "/somewhere/dev/includeme",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// within a known mountpoint with valid type
|
||||||
|
path: "/somewhere/dev/includeme/too!",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "keep some tmpfs mounts conditionally",
|
||||||
|
root: "/",
|
||||||
|
mounts: []*mountinfo.Info{
|
||||||
|
{
|
||||||
|
Mountpoint: "/run/somewhere",
|
||||||
|
FSType: "tmpfs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/run/terrafirma",
|
||||||
|
FSType: "/dev/disk3s8",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/tmp",
|
||||||
|
FSType: "tmpfs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/else/othertmp",
|
||||||
|
FSType: "tmpfs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/else/othertmp/includeme",
|
||||||
|
FSType: "/dev/disk3s7",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []expect{
|
||||||
|
{
|
||||||
|
// since /run is explicitly ignored, this should be skipped
|
||||||
|
path: "/run/somewhere/else",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/run/terrafirma",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/run/terrafirma/nested",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/tmp",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/else/othertmp/includeme",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/else/othertmp/includeme/nested",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// no mount path, so we should include it
|
||||||
|
path: "/somewhere/dev/includeme",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// keep additional tmpfs mounts that are not explicitly ignored
|
||||||
|
path: "/else/othertmp",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignore known trixy tmpfs paths",
|
||||||
|
root: "/",
|
||||||
|
mounts: []*mountinfo.Info{
|
||||||
|
{
|
||||||
|
Mountpoint: "/",
|
||||||
|
FSType: "/dev/disk3s7",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/dev",
|
||||||
|
FSType: "tmpfs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/run",
|
||||||
|
FSType: "tmpfs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/var/run",
|
||||||
|
FSType: "tmpfs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/var/lock",
|
||||||
|
FSType: "tmpfs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/sys",
|
||||||
|
FSType: "tmpfs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/tmp",
|
||||||
|
FSType: "tmpfs",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []expect{
|
||||||
|
{
|
||||||
|
path: "/dev",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/run",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/var/run",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/var/lock",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/sys",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
// show that we honor ignoring nested paths
|
||||||
|
{
|
||||||
|
path: "/sys/nested",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
// show that paths outside of the known mountpoints are not skipped
|
||||||
|
{
|
||||||
|
path: "/stuff",
|
||||||
|
},
|
||||||
|
// show that we allow other tmpfs paths that are not on the blocklist
|
||||||
|
{
|
||||||
|
path: "/tmp/allowed",
|
||||||
|
},
|
||||||
|
// show sibling paths with same prefix (e.g. /sys vs /system) to that of not allowed paths are not skipped
|
||||||
|
{
|
||||||
|
path: "/system",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test unix paths",
|
||||||
|
mounts: unixSubject,
|
||||||
|
root: "/",
|
||||||
|
want: []expect{
|
||||||
|
{
|
||||||
|
// relative path to proc is allowed
|
||||||
|
path: "proc/place",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// relative path within proc is not allowed
|
||||||
|
path: "/proc/place",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// path exactly to proc is not allowed
|
||||||
|
path: "/proc",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// similar to proc
|
||||||
|
path: "/pro/c",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// similar to proc
|
||||||
|
path: "/pro",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// dev is not allowed
|
||||||
|
path: "/dev",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// sys is not allowed
|
||||||
|
path: "/sys",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test unix paths with base",
|
||||||
|
mounts: unixSubject,
|
||||||
|
root: "/",
|
||||||
|
base: "/a/b/c",
|
||||||
|
want: []expect{
|
||||||
|
{
|
||||||
|
// do not consider base when matching paths (non-matching)
|
||||||
|
path: "/a/b/c/dev",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// do not consider base when matching paths (matching)
|
||||||
|
path: "/dev",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mimic nixos setup",
|
||||||
|
root: "/",
|
||||||
|
mounts: []*mountinfo.Info{
|
||||||
|
{
|
||||||
|
Mountpoint: "/",
|
||||||
|
FSType: "tmpfs", // this is an odd setup, but valid
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Mountpoint: "/home",
|
||||||
|
FSType: "/dev/disk3s7",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: []expect{
|
||||||
|
{
|
||||||
|
path: "/home/somewhere",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/home",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/somewhere",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// still not allowed...
|
||||||
|
path: "/run",
|
||||||
|
wantErr: assertSkipErr(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.base == "" {
|
||||||
|
tt.base = tt.root
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NotEmpty(t, tt.want)
|
||||||
|
ps := newPathSkipperFromMounts(tt.root, tt.mounts)
|
||||||
|
|
||||||
|
for _, exp := range tt.want {
|
||||||
|
t.Run(exp.path, func(t *testing.T) {
|
||||||
|
|
||||||
|
got := ps.pathIndexVisitor(tt.base, exp.path, nil, nil)
|
||||||
|
if exp.wantErr == nil {
|
||||||
|
assert.NoError(t, got)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
exp.wantErr(t, got)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertSkipErr() assert.ErrorAssertionFunc {
|
||||||
|
return assertErrorIs(fs.SkipDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertErrorIs(want error) assert.ErrorAssertionFunc {
|
||||||
|
return func(t assert.TestingT, got error, msgAndArgs ...interface{}) bool {
|
||||||
|
return assert.ErrorIs(t, got, want, msgAndArgs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
60
test/cli/archive_test.go
Normal file
60
test/cli/archive_test.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArchiveScan(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
archiveFixture string
|
||||||
|
env map[string]string
|
||||||
|
assertions []traitAssertion
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "scan an archive within the temp dir",
|
||||||
|
args: []string{
|
||||||
|
"scan",
|
||||||
|
"-o",
|
||||||
|
"json",
|
||||||
|
"file:" + createArchive(t, "test-fixtures/archive", t.TempDir()),
|
||||||
|
},
|
||||||
|
assertions: []traitAssertion{
|
||||||
|
assertSuccessfulReturnCode,
|
||||||
|
assertJsonReport,
|
||||||
|
assertPackageCount(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
cmd, stdout, stderr := runSyft(t, test.env, test.args...)
|
||||||
|
for _, traitAssertionFn := range test.assertions {
|
||||||
|
traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
|
||||||
|
}
|
||||||
|
logOutputOnFailure(t, cmd, stdout, stderr)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createArchive(t *testing.T, path string, destDir string) string {
|
||||||
|
// create a tarball of the test fixtures (not by shelling out)
|
||||||
|
archivePath := filepath.Join(destDir, "test.tar")
|
||||||
|
|
||||||
|
fh, err := os.Create(archivePath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer fh.Close()
|
||||||
|
|
||||||
|
writer := tar.NewWriter(fh)
|
||||||
|
require.NoError(t, writer.AddFS(os.DirFS(path)))
|
||||||
|
require.NoError(t, writer.Close())
|
||||||
|
|
||||||
|
return archivePath
|
||||||
|
}
|
||||||
47
test/cli/test-fixtures/archive/dist-info/METADATA
Normal file
47
test/cli/test-fixtures/archive/dist-info/METADATA
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: Pygments
|
||||||
|
Version: 2.6.1
|
||||||
|
Summary: Pygments is a syntax highlighting package written in Python.
|
||||||
|
Home-page: https://pygments.org/
|
||||||
|
Author: Georg Brandl
|
||||||
|
Author-email: georg@python.org
|
||||||
|
License: BSD License
|
||||||
|
Keywords: syntax highlighting
|
||||||
|
Platform: any
|
||||||
|
Classifier: License :: OSI Approved :: BSD License
|
||||||
|
Classifier: Intended Audience :: Developers
|
||||||
|
Classifier: Intended Audience :: End Users/Desktop
|
||||||
|
Classifier: Intended Audience :: System Administrators
|
||||||
|
Classifier: Development Status :: 6 - Mature
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.5
|
||||||
|
Classifier: Programming Language :: Python :: 3.6
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Programming Language :: Python :: 3.8
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||||
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
Classifier: Operating System :: OS Independent
|
||||||
|
Classifier: Topic :: Text Processing :: Filters
|
||||||
|
Classifier: Topic :: Utilities
|
||||||
|
Requires-Python: >=3.5
|
||||||
|
|
||||||
|
|
||||||
|
Pygments
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
Pygments is a syntax highlighting package written in Python.
|
||||||
|
|
||||||
|
It is a generic syntax highlighter suitable for use in code hosting, forums,
|
||||||
|
wikis or other applications that need to prettify source code. Highlights
|
||||||
|
are:
|
||||||
|
|
||||||
|
* a wide range of over 500 languages and other text formats is supported
|
||||||
|
* special attention is paid to details, increasing quality by a fair amount
|
||||||
|
* support for new languages and formats are added easily
|
||||||
|
* a number of output formats, presently HTML, LaTeX, RTF, SVG, all image formats that PIL supports and ANSI sequences
|
||||||
|
* it is usable as a command-line tool and as a library
|
||||||
|
|
||||||
|
:copyright: Copyright 2006-2019 by the Pygments team, see AUTHORS.
|
||||||
|
:license: BSD, see LICENSE for details.
|
||||||
|
|
||||||
5
test/cli/test-fixtures/archive/dist-info/RECORD
Normal file
5
test/cli/test-fixtures/archive/dist-info/RECORD
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
../../../bin/pygmentize,sha256=dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8,220
|
||||||
|
Pygments-2.6.1.dist-info/AUTHORS,sha256=PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY,8449
|
||||||
|
Pygments-2.6.1.dist-info/RECORD,,
|
||||||
|
pygments/__pycache__/__init__.cpython-38.pyc,,
|
||||||
|
pygments/util.py,sha256=586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA,10778
|
||||||
1
test/cli/test-fixtures/archive/dist-info/top_level.txt
Normal file
1
test/cli/test-fixtures/archive/dist-info/top_level.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
top-level-pkg
|
||||||
Loading…
x
Reference in New Issue
Block a user