Skip to content

Commit

Permalink
feat: Use lockfile scalibr interface (#1330)
Browse files Browse the repository at this point in the history
This PR contains all the code required to move to osv-scalibr while
making the existing code compile and pass all tests (container tests not
passing because of a bug in the scalibr alpine extractor).

Changes not mentioned in the following list will be split off in
separate PRs which should land before this PR.

Those are:
- [x] #1337
- [x] #1331
- [x] #1338
- [x] #1341
- [x] #1345

Changes in this PR:
- Fixture changes:
- Scalibr Python requirements.txt extractor currently doesn't support
packages without versions, so added some version strings to the test
files
- Image package required quite a bit of reworking to successfully
update.
- Add the ability to iterate through a directory via the pathtree
library
  - Support scalibr FS interface for Layers
- Add conversion code to convert inventories from osv-scalibr back to
v1's lockfile and Inventory
- This is done to minimize snapshot changes. Followup PRs should remove
this conversion
- Add `internal/lockfilescalibr` package:
  - `errors.go` adds common extraction errors we want to translate.
- `translation.go` adds helper functions and translation logic between
osv-scanner v1 extractor names, and osv-scalibr extractor names.

Changes in followup PRs:
- Delete lockfiles package and migrate everything to use osv-scalibr
extractors
- Remove conversion code in image

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Gareth Jones <Jones258@Gmail.com>
Co-authored-by: Xueqin Cui <72771658+cuixq@users.noreply.github.com>
Co-authored-by: Michael Kedar <michaelkedar@google.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
  • Loading branch information
5 people committed Nov 29, 2024
1 parent cf6f568 commit 587bf77
Show file tree
Hide file tree
Showing 32 changed files with 718 additions and 472 deletions.
335 changes: 77 additions & 258 deletions cmd/osv-scanner/__snapshots__/main_test.snap

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1 +1 @@
flask
flask==1.0.0
Original file line number Diff line number Diff line change
@@ -1 +1 @@
black
black==1.0.0
4 changes: 2 additions & 2 deletions cmd/osv-scanner/fixtures/locks-requirements/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
flask
flask-cors
flask==1.0.0
flask-cors==1.0.0
pandas==0.23.4
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pytest
pytest==1.0.0
7 changes: 6 additions & 1 deletion cmd/osv-scanner/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,12 @@ func TestRun_LockfileWithExplicitParseAs(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

testCli(t, tt)
stdout, stderr := runCli(t, tt)

testutility.NewSnapshot().MatchText(t, stdout)
testutility.NewSnapshot().WithWindowsReplacements(map[string]string{
"CreateFile": "stat",
}).MatchText(t, stderr)
})
}
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require (
github.com/go-git/go-git/v5 v5.12.0
github.com/google/go-cmp v0.6.0
github.com/google/go-containerregistry v0.20.2
github.com/google/osv-scalibr v0.1.4-0.20241016092100-7e7f0c6a01ec
github.com/google/osv-scalibr v0.1.4-0.20241031120023-761ca671aacb
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd
github.com/jedib0t/go-pretty/v6 v6.6.0
github.com/muesli/reflow v0.3.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-containerregistry v0.20.2 h1:B1wPJ1SN/S7pB+ZAimcciVD+r+yV/l/DSArMxlbwseo=
github.com/google/go-containerregistry v0.20.2/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8=
github.com/google/osv-scalibr v0.1.4-0.20241016092100-7e7f0c6a01ec h1:pbByndoAmqND/Vkj3wYLS2aDAq+/2dll7rKzIM3ezCU=
github.com/google/osv-scalibr v0.1.4-0.20241016092100-7e7f0c6a01ec/go.mod h1:MbEYB+PKqEGjwMdpcoO5DWpi0+57jYgYcw2jlRy8O9Q=
github.com/google/osv-scalibr v0.1.4-0.20241031120023-761ca671aacb h1:A7IvUJk8r3wMuuAMWxwbkE3WBp+oF/v7CcEt3nCy+lI=
github.com/google/osv-scalibr v0.1.4-0.20241031120023-761ca671aacb/go.mod h1:MbEYB+PKqEGjwMdpcoO5DWpi0+57jYgYcw2jlRy8O9Q=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
Expand Down
32 changes: 16 additions & 16 deletions internal/image/__snapshots__/image_test.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"Lockfiles": [
{
"filePath": "/lib/apk/db/installed",
"parsedAs": "apk-installed",
"parsedAs": "os/apk",
"packages": [
{
"name": "alpine-baselayout",
Expand Down Expand Up @@ -186,7 +186,7 @@
"Lockfiles": [
{
"filePath": "/go/bin/more-vuln-overwrite-less-vuln",
"parsedAs": "go-binary",
"parsedAs": "go/binary",
"packages": [
{
"name": "github.com/BurntSushi/toml",
Expand Down Expand Up @@ -214,7 +214,7 @@
},
{
"filePath": "/go/bin/ptf-1.2.0",
"parsedAs": "go-binary",
"parsedAs": "go/binary",
"packages": [
{
"name": "github.com/BurntSushi/toml",
Expand Down Expand Up @@ -242,7 +242,7 @@
},
{
"filePath": "/go/bin/ptf-1.3.0",
"parsedAs": "go-binary",
"parsedAs": "go/binary",
"packages": [
{
"name": "github.com/BurntSushi/toml",
Expand Down Expand Up @@ -270,7 +270,7 @@
},
{
"filePath": "/go/bin/ptf-1.3.0-moved",
"parsedAs": "go-binary",
"parsedAs": "go/binary",
"packages": [
{
"name": "github.com/BurntSushi/toml",
Expand Down Expand Up @@ -298,7 +298,7 @@
},
{
"filePath": "/go/bin/ptf-1.4.0",
"parsedAs": "go-binary",
"parsedAs": "go/binary",
"packages": [
{
"name": "github.com/BurntSushi/toml",
Expand Down Expand Up @@ -326,7 +326,7 @@
},
{
"filePath": "/go/bin/ptf-vulnerable",
"parsedAs": "go-binary",
"parsedAs": "go/binary",
"packages": [
{
"name": "github.com/BurntSushi/toml",
Expand Down Expand Up @@ -354,7 +354,7 @@
},
{
"filePath": "/lib/apk/db/installed",
"parsedAs": "apk-installed",
"parsedAs": "os/apk",
"packages": [
{
"name": "alpine-baselayout",
Expand Down Expand Up @@ -536,7 +536,7 @@
"Lockfiles": [
{
"filePath": "/lib/apk/db/installed",
"parsedAs": "apk-installed",
"parsedAs": "os/apk",
"packages": [
{
"name": "alpine-baselayout",
Expand Down Expand Up @@ -754,7 +754,7 @@
"Lockfiles": [
{
"filePath": "/lib/apk/db/installed",
"parsedAs": "apk-installed",
"parsedAs": "os/apk",
"packages": [
{
"name": "alpine-baselayout",
Expand Down Expand Up @@ -963,8 +963,8 @@
]
},
{
"filePath": "/usr/app/node_modules/.package-lock.json",
"parsedAs": "node_modules",
"filePath": "/prod/app/node_modules/.package-lock.json",
"parsedAs": "javascript/nodemodules",
"packages": [
{
"name": "cryo",
Expand Down Expand Up @@ -1011,7 +1011,7 @@
"Lockfiles": [
{
"filePath": "/lib/apk/db/installed",
"parsedAs": "apk-installed",
"parsedAs": "os/apk",
"packages": [
{
"name": "alpine-baselayout",
Expand Down Expand Up @@ -1229,7 +1229,7 @@
"Lockfiles": [
{
"filePath": "/lib/apk/db/installed",
"parsedAs": "apk-installed",
"parsedAs": "os/apk",
"packages": [
{
"name": "alpine-baselayout",
Expand Down Expand Up @@ -1447,7 +1447,7 @@
"Lockfiles": [
{
"filePath": "/lib/apk/db/installed",
"parsedAs": "apk-installed",
"parsedAs": "os/apk",
"packages": [
{
"name": "alpine-baselayout",
Expand Down Expand Up @@ -1665,7 +1665,7 @@
"Lockfiles": [
{
"filePath": "/lib/apk/db/installed",
"parsedAs": "apk-installed",
"parsedAs": "os/apk",
"packages": [
{
"name": "alpine-baselayout",
Expand Down
153 changes: 66 additions & 87 deletions internal/image/extractor.go
Original file line number Diff line number Diff line change
@@ -1,134 +1,113 @@
package image

import (
"context"
"errors"
"fmt"
"os"
"path"
"sort"

"io/fs"
"strings"

"github.com/google/osv-scalibr/extractor"
"github.com/google/osv-scalibr/extractor/filesystem"
"github.com/google/osv-scalibr/extractor/filesystem/language/golang/gobinary"
"github.com/google/osv-scalibr/extractor/filesystem/os/apk"
"github.com/google/osv-scalibr/extractor/filesystem/os/dpkg"
"github.com/google/osv-scanner/internal/lockfilescalibr"
"github.com/google/osv-scanner/internal/lockfilescalibr/language/javascript/nodemodules"
"github.com/google/osv-scanner/pkg/lockfile"
)

// artifactExtractors contains only extractors for artifacts that are important in
// the final layer of a container image
var artifactExtractors map[string]lockfile.Extractor = map[string]lockfile.Extractor{
"node_modules": lockfile.NodeModulesExtractor{},
"apk-installed": lockfile.ApkInstalledExtractor{},
"dpkg": lockfile.DpkgStatusExtractor{},
"go-binary": lockfile.GoBinaryExtractor{},
}

type extractorPair struct {
extractor lockfile.Extractor
name string
var artifactExtractors []filesystem.Extractor = []filesystem.Extractor{
// TODO: Using nodemodules extractor to minimize changes of snapshots
// After annotations are added, we should switch to using packagejson.
// packagejson.New(packagejson.DefaultConfig()),
nodemodules.Extractor{},

apk.New(apk.DefaultConfig()),
gobinary.New(gobinary.DefaultConfig()),
// TODO: Add tests for debian containers
dpkg.New(dpkg.DefaultConfig()),
}

func findArtifactExtractor(path string) []extractorPair {
func findArtifactExtractor(path string, fileInfo fs.FileInfo) []filesystem.Extractor {
// Use ShouldExtract to collect and return a slice of artifactExtractors
var extractors []extractorPair
for name, extractor := range artifactExtractors {
if extractor.ShouldExtract(path) {
extractors = append(extractors, extractorPair{extractor, name})
var extractors []filesystem.Extractor
for _, extractor := range artifactExtractors {
if extractor.FileRequired(path, fileInfo) {
extractors = append(extractors, extractor)
}
}

return extractors
}

func extractArtifactDeps(path string, layer *Layer) (lockfile.Lockfile, error) {
foundExtractors := findArtifactExtractor(path)
// Note: Output is non deterministic
func extractArtifactDeps(extractPath string, layer *Layer) ([]*extractor.Inventory, error) {
pathFileInfo, err := layer.Stat(extractPath)
if err != nil {
return nil, fmt.Errorf("attempted to get FileInfo but failed: %w", err)
}

scalibrPath := strings.TrimPrefix(extractPath, "/")
foundExtractors := findArtifactExtractor(scalibrPath, pathFileInfo)
if len(foundExtractors) == 0 {
return lockfile.Lockfile{}, fmt.Errorf("%w for %s", lockfile.ErrExtractorNotFound, path)
return nil, fmt.Errorf("%w for %s", lockfilescalibr.ErrExtractorNotFound, extractPath)
}

packages := []lockfile.PackageDetails{}
inventories := []*extractor.Inventory{}
var extractedAs string
for _, extPair := range foundExtractors {
for _, extractor := range foundExtractors {
// File has to be reopened per extractor as each extractor moves the read cursor
f, err := OpenLayerFile(path, layer)
f, err := layer.Open(extractPath)
if err != nil {
return lockfile.Lockfile{}, fmt.Errorf("attempted to open file but failed: %w", err)
return nil, fmt.Errorf("attempted to open file but failed: %w", err)
}

scanInput := &filesystem.ScanInput{
FS: layer,
Path: scalibrPath,
Root: "/",
Reader: f,
Info: pathFileInfo,
}

newPackages, err := extPair.extractor.Extract(f)
newPackages, err := extractor.Extract(context.Background(), scanInput)
f.Close()

if err != nil {
if errors.Is(err, lockfile.ErrIncompatibleFileFormat) {
continue
}

return lockfile.Lockfile{}, fmt.Errorf("(extracting as %s) %w", extPair.name, err)
return nil, fmt.Errorf("(extracting as %s) %w", extractor.Name(), err)
}

extractedAs = extPair.name
packages = newPackages
// TODO(rexpan): Determine if it's acceptable to have multiple extractors
for i := range newPackages {
newPackages[i].Extractor = extractor
}

extractedAs = extractor.Name()
inventories = newPackages
// TODO(rexpan): Determine if this it's acceptable to have multiple extractors
// extract from the same file successfully
break
}

if extractedAs == "" {
return lockfile.Lockfile{}, fmt.Errorf("%w for %s", lockfile.ErrExtractorNotFound, path)
return nil, fmt.Errorf("%w for %s", lockfilescalibr.ErrExtractorNotFound, extractPath)
}

// Sort to have deterministic output, and to match behavior of lockfile.extractDeps
sort.Slice(packages, func(i, j int) bool {
if packages[i].Name == packages[j].Name {
return packages[i].Version < packages[j].Version
// Perform any one-off translations here
for _, inv := range inventories {
// Scalibr uses go to indicate go compiler version
// We specifically cares about the stdlib version inside the package
// so convert the package name from go to stdlib
if inv.Ecosystem() == "Go" && inv.Name == "go" {
inv.Name = "stdlib"
}

return packages[i].Name < packages[j].Name
})

return lockfile.Lockfile{
FilePath: path,
ParsedAs: extractedAs,
Packages: packages,
}, nil
}

// A File represents a file that exists in an image
type File struct {
*os.File

layer *Layer
path string
}

func (f File) Open(openPath string) (lockfile.NestedDepFile, error) {
// use path instead of filepath, because container is always in Unix paths (for now)
if path.IsAbs(openPath) {
return OpenLayerFile(openPath, f.layer)
}

absPath := path.Join(f.path, openPath)

return OpenLayerFile(absPath, f.layer)
}

func (f File) Path() string {
return f.path
}

func OpenLayerFile(path string, layer *Layer) (File, error) {
fileNode, err := layer.getFileNode(path)
if err != nil {
return File{}, err
}

file, err := fileNode.Open()
if err != nil {
return File{}, err
}

return File{
File: file,
path: path,
layer: layer,
}, nil
return inventories, nil
}

var _ lockfile.DepFile = File{}
var _ lockfile.NestedDepFile = File{}
7 changes: 7 additions & 0 deletions internal/image/fixtures/alpine-3.18-os-release
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/ # cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.18.1
PRETTY_NAME="Alpine Linux v3.18"
HOME_URL="https://alpinelinux.org/"
BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
Loading

0 comments on commit 587bf77

Please sign in to comment.