Fix hang when running as a subprocess (#484)

* use named pipe bit on stdin as indicator for piped input

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* ensure stdin is ignored when the CLI hints are present

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add CLI test to cover subprocess integration behavior

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* added test case for java regression

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* remove extra line in makefile

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman 2021-10-29 10:51:58 -04:00 committed by GitHub
parent 9349060765
commit afc9de6058
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 318 additions and 493 deletions

View file

@ -74,7 +74,7 @@ jobs:
- name: Validate grype output against the CycloneDX schema
run: make validate-cyclonedx-schema
- name: Build key for tar cache
- name: Build key for integration tar cache
run: make integration-fingerprint
- name: Restore integration test cache
@ -83,6 +83,15 @@ jobs:
path: ${{ github.workspace }}/test/integration/test-fixtures/cache
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }}
- name: Build key for CLI tar cache
run: make cli-fingerprint
- name: Restore cli test cache
uses: actions/cache@v2
with:
path: ${{ github.workspace }}/test/cli/test-fixtures/cache
key: ${{ runner.os }}-cli-test-cache-${{ hashFiles('test/cli/test-fixtures/cache.fingerprint') }}
- name: Run integration tests
run: make integration

View file

@ -149,6 +149,11 @@ integration: ## Run integration tests
integration-fingerprint:
find test/integration/*.go test/integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/integration/test-fixtures/cache.fingerprint
# note: this is used by CI to determine if the cli test fixture cache (docker image tars) should be busted
.PHONY: cli-fingerprint
cli-fingerprint:
find test/cli/*.go test/cli/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/cli/test-fixtures/cache.fingerprint
.PHONY: cli
cli: $(SNAPSHOTDIR) ## Run CLI tests
chmod 755 "$(SNAPSHOT_CMD)"

1
go.mod
View file

@ -14,6 +14,7 @@ require (
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/facebookincubator/nvdtools v0.1.4
github.com/gabriel-vasile/mimetype v1.3.0
github.com/go-test/deep v1.0.7
github.com/google/go-cmp v0.4.1
github.com/google/uuid v1.2.0

View file

@ -1,82 +1,21 @@
package pkg
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"github.com/anchore/grype/internal/log"
"github.com/anchore/grype/internal"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/syft/source"
)
var errDoesNotProvide = fmt.Errorf("cannot provide packages from the given source")
type providerConfig struct {
userInput string
scopeOpt source.Scope
reader io.Reader
registryOptions *image.RegistryOptions
}
type provider func(cfg providerConfig) ([]Package, Context, error)
// Provide a set of packages and context metadata describing where they were sourced from.
func Provide(userInput string, scopeOpt source.Scope, registryOptions *image.RegistryOptions) ([]Package, Context, error) {
providers := []provider{
syftJSONProvider,
syftProvider, // important: we should try syft last
packages, ctx, err := syftSBOMProvider(userInput)
if !errors.Is(err, errDoesNotProvide) {
return packages, ctx, err
}
// capture stdin bytes, so they can be used across multiple providers
capturedStdin := bytesFromStdin()
for _, provide := range providers {
config := determineProviderConfig(userInput, scopeOpt, registryOptions, capturedStdin)
packages, ctx, err := provide(config)
if !errors.Is(err, errDoesNotProvide) {
return packages, ctx, err
}
}
return nil, Context{}, errDoesNotProvide
}
func bytesFromStdin() []byte {
isPipedInput, err := internal.IsPipedInput()
if err != nil {
log.Warnf("unable to determine if there is piped input: %+v", err)
isPipedInput = false
}
if isPipedInput {
capturedStdin, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return nil
}
return capturedStdin
}
return nil
}
func determineProviderConfig(userInput string, scopeOpt source.Scope, registryOptions *image.RegistryOptions, stdin []byte) providerConfig {
config := providerConfig{
userInput: userInput,
scopeOpt: scopeOpt,
registryOptions: registryOptions,
}
if len(stdin) > 0 {
config.reader = bytes.NewReader(stdin)
}
return config
return syftProvider(userInput, scopeOpt, registryOptions)
}

View file

@ -1,84 +0,0 @@
package pkg
import (
"io/ioutil"
"testing"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/assert"
)
type providerTestConfig struct {
userInput string
scopeOpt source.Scope
readerBytes []byte
}
func TestDetermineProviderConfig(t *testing.T) {
cases := []struct {
name string
userInput string
scopeOpt source.Scope
stdin []byte
expected providerTestConfig
}{
{
"explicit sbom path",
"sbom:/Users/bob/sbom.json",
source.SquashedScope,
nil,
providerTestConfig{
"sbom:/Users/bob/sbom.json",
source.SquashedScope,
nil,
},
},
{
"explicit stdin",
"",
source.SquashedScope,
[]byte("{some json}"),
providerTestConfig{
"",
source.SquashedScope,
[]byte("{some json}"),
},
},
{
"stdin and userInput",
"some-value",
source.SquashedScope,
[]byte("{some json}"),
providerTestConfig{
"some-value",
source.SquashedScope,
[]byte("{some json}"),
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
rawConfig := determineProviderConfig(tc.userInput, tc.scopeOpt, nil, tc.stdin)
actual := mapToProviderTestConfig(t, rawConfig)
assert.Equal(t, tc.expected, actual)
})
}
}
func mapToProviderTestConfig(t *testing.T, rawConfig providerConfig) providerTestConfig {
t.Helper()
var readerBytes []byte
if rawConfig.reader != nil {
readerBytes, _ = ioutil.ReadAll(rawConfig.reader)
}
return providerTestConfig{
rawConfig.userInput,
rawConfig.scopeOpt,
readerBytes,
}
}

View file

@ -1,307 +0,0 @@
package pkg
import (
"encoding/json"
"fmt"
"io"
"os"
"strings"
"github.com/anchore/grype/internal/log"
"github.com/anchore/grype/grype/cpe"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/mitchellh/go-homedir"
)
type syftSource struct {
Type string `json:"type"`
Target interface{} `json:"target"`
}
// syftSourceUnpacker is used to unmarshal Source objects
type syftSourceUnpacker struct {
Type string `json:"type"`
Target json.RawMessage `json:"target"`
}
// UnmarshalJSON populates a source object from JSON bytes.
func (s *syftSource) UnmarshalJSON(b []byte) error {
var unpacker syftSourceUnpacker
if err := json.Unmarshal(b, &unpacker); err != nil {
return err
}
s.Type = unpacker.Type
switch s.Type {
case "directory":
s.Target = string(unpacker.Target[:])
case "image":
var payload source.ImageMetadata
if err := json.Unmarshal(unpacker.Target, &payload); err != nil {
return err
}
s.Target = payload
default:
return fmt.Errorf("unsupported package metadata type: %+v", s.Type)
}
return nil
}
// ToSourceMetadata takes a syftSource object represented from JSON and creates a source.Metadata object.
func (s *syftSource) toSourceMetadata() source.Metadata {
var m source.Metadata
switch s.Type {
case "directory":
m.Scheme = source.DirectoryScheme
m.Path = s.Target.(string)
case "image":
m.Scheme = source.ImageScheme
m.ImageMetadata = s.Target.(source.ImageMetadata)
}
return m
}
type syftDistribution struct {
Name string `json:"name"` // Name of the Linux syftDistribution
Version string `json:"version"` // Version of the Linux syftDistribution (major or major.minor version)
IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file
}
// partialSyftDoc is the final package shape for a select elements from a syft JSON document.
type partialSyftDoc struct {
Source syftSource `json:"source"`
Artifacts []partialSyftPackage `json:"artifacts"`
Distro syftDistribution `json:"distro"`
}
// partialSyftPackage is the final package shape for a select elements from a syft JSON package.
type partialSyftPackage struct {
packageBasicMetadata
packageCustomMetadata
}
// packageBasicMetadata contains non-ambiguous values (type-wise) from pkg.Package.
type packageBasicMetadata struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Type pkg.Type `json:"type"`
Locations []source.Location `json:"locations"`
Licenses []string `json:"licenses"`
Language pkg.Language `json:"language"`
CPEs []string `json:"cpes"`
PURL string `json:"purl"`
}
// packageCustomMetadata contains ambiguous values (type-wise) from pkg.Package.
type packageCustomMetadata struct {
MetadataType pkg.MetadataType `json:"metadataType"`
Metadata interface{} `json:"metadata"`
}
// packageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling.
type packageMetadataUnpacker struct {
MetadataType pkg.MetadataType `json:"metadataType"`
Metadata json.RawMessage `json:"metadata"`
}
func (p *packageMetadataUnpacker) String() string {
return fmt.Sprintf("metadataType: %s, metadata: %s", p.MetadataType, string(p.Metadata))
}
// partialSyftJavaMetadata encapsulates all Java ecosystem metadata for a package as well as an (optional) parent relationship.
type partialSyftJavaMetadata struct {
VirtualPath string `mapstructure:"VirtualPath" json:"virtualPath"`
Manifest *partialSyftJavaManifest `mapstructure:"Manifest" json:"manifest,omitempty"`
PomProperties *partialSyftPomProperties `mapstructure:"PomProperties" json:"pomProperties,omitempty"`
}
// partialSyftPomProperties represents the fields of interest extracted from a Java archive's pom.xml file.
type partialSyftPomProperties struct {
GroupID string `mapstructure:"groupId" json:"groupId"`
ArtifactID string `mapstructure:"artifactId" json:"artifactId"`
}
// partialSyftJavaManifest represents the fields of interest extracted from a Java archive's META-INF/MANIFEST.MF file.
type partialSyftJavaManifest struct {
Main map[string]string `json:"main,omitempty"`
}
// String returns the stringer representation for a syft package.
func (p partialSyftPackage) String() string {
return fmt.Sprintf("Pkg(type=%s, name=%s, version=%s)", p.Type, p.Name, p.Version)
}
// UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types.
func (p *partialSyftPackage) UnmarshalJSON(b []byte) error {
var basic packageBasicMetadata
if err := json.Unmarshal(b, &basic); err != nil {
return err
}
p.packageBasicMetadata = basic
var unpacker packageMetadataUnpacker
if err := json.Unmarshal(b, &unpacker); err != nil {
log.Warnf("failed to unmarshall into packageMetadataUnpacker: %v", err)
return err
}
p.MetadataType = unpacker.MetadataType
switch p.MetadataType {
case pkg.ApkMetadataType:
var payload ApkMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.RpmdbMetadataType:
var payload RpmdbMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.DpkgMetadataType:
var payload DpkgMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.JavaMetadataType:
var partialPayload partialSyftJavaMetadata
if err := json.Unmarshal(unpacker.Metadata, &partialPayload); err != nil {
return err
}
var artifact, group, name string
if partialPayload.PomProperties != nil {
artifact = partialPayload.PomProperties.ArtifactID
group = partialPayload.PomProperties.GroupID
}
if partialPayload.Manifest != nil {
if n, ok := partialPayload.Manifest.Main["Name"]; ok {
name = n
}
}
p.Metadata = JavaMetadata{
VirtualPath: partialPayload.VirtualPath,
PomArtifactID: artifact,
PomGroupID: group,
ManifestName: name,
}
}
return nil
}
// parseSyftJSON attempts to loosely parse the available JSON for only the fields needed, not the exact syft JSON shape.
// This allows for some resiliency as the syft document shape changes over time (but not fool-proof).
func parseSyftJSON(reader io.Reader) ([]Package, Context, error) {
var doc partialSyftDoc
decoder := json.NewDecoder(reader)
if err := decoder.Decode(&doc); err != nil {
return nil, Context{}, errDoesNotProvide
}
var packages = make([]Package, len(doc.Artifacts))
for i, a := range doc.Artifacts {
cpes, err := cpe.NewSlice(a.CPEs...)
if err != nil {
return nil, Context{}, err
}
packages[i] = Package{
ID: ID(a.ID),
Name: a.Name,
Version: a.Version,
Locations: a.Locations,
Language: a.Language,
Licenses: a.Licenses,
Type: a.Type,
CPEs: cpes,
PURL: a.PURL,
Metadata: a.Metadata,
}
}
var theDistro *distro.Distro
if doc.Distro.Name != "" {
d, err := distro.NewDistro(distro.Type(doc.Distro.Name), doc.Distro.Version, doc.Distro.IDLike)
if err != nil {
return nil, Context{}, err
}
theDistro = &d
}
srcMetadata := doc.Source.toSourceMetadata()
return packages, Context{
Source: &srcMetadata,
Distro: theDistro,
}, nil
}
// syftJSONProvider extracts the necessary package and package context from syft JSON output. Note that this process carves out
// only the necessary data needed and does not require unmarshalling the entire syft JSON data shape so this function is somewhat
// resilient to multiple syft JSON schemas (to a degree).
// TODO: add version detection and multi-parser support (when needed in the future)
func syftJSONProvider(config providerConfig) ([]Package, Context, error) {
reader, err := getSyftJSON(config)
if err != nil {
return nil, Context{}, err
}
return parseSyftJSON(reader)
}
func getSyftJSON(config providerConfig) (io.Reader, error) {
if config.reader != nil {
// the caller has explicitly indicated to use the given reader as input
return config.reader, nil
}
if explicitlySpecifyingSBOM(config.userInput) {
filepath := strings.TrimPrefix(config.userInput, "sbom:")
sbom, err := openSbom(filepath)
if err != nil {
return nil, fmt.Errorf("unable to use specified SBOM: %w", err)
}
return sbom, nil
}
// as a last resort, see if the raw user input specified an SBOM file
sbom, err := openSbom(config.userInput)
if err == nil {
return sbom, nil
}
// no usable SBOM is available
return nil, errDoesNotProvide
}
func openSbom(path string) (*os.File, error) {
expandedPath, err := homedir.Expand(path)
if err != nil {
return nil, fmt.Errorf("unable to open SBOM: %w", err)
}
sbom, err := os.Open(expandedPath)
if err != nil {
return nil, fmt.Errorf("unable to open SBOM: %w", err)
}
return sbom, nil
}
func explicitlySpecifyingSBOM(userInput string) bool {
return strings.HasPrefix(userInput, "sbom:")
}

View file

@ -1,22 +1,23 @@
package pkg
import (
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/source"
)
func syftProvider(config providerConfig) ([]Package, Context, error) {
if config.scopeOpt == "" {
func syftProvider(userInput string, scopeOpt source.Scope, registryOptions *image.RegistryOptions) ([]Package, Context, error) {
if scopeOpt == "" {
return nil, Context{}, errDoesNotProvide
}
src, cleanup, err := source.New(config.userInput, config.registryOptions)
src, cleanup, err := source.New(userInput, registryOptions)
if err != nil {
return nil, Context{}, err
}
defer cleanup()
catalog, theDistro, err := syft.CatalogPackages(src, config.scopeOpt)
catalog, theDistro, err := syft.CatalogPackages(src, scopeOpt)
if err != nil {
return nil, Context{}, err
}

View file

@ -0,0 +1,129 @@
package pkg
import (
"fmt"
"io"
"os"
"strings"
"github.com/gabriel-vasile/mimetype"
"github.com/anchore/grype/internal"
"github.com/anchore/grype/internal/log"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft"
"github.com/mitchellh/go-homedir"
)
func syftSBOMProvider(userInput string) ([]Package, Context, error) {
reader, err := getSBOMReader(userInput)
if err != nil {
return nil, Context{}, err
}
catalog, srcMetadata, theDistro, _, formatOption, err := syft.Decode(reader)
if err != nil {
return nil, Context{}, fmt.Errorf("unable to decode sbom: %w", err)
}
if formatOption == format.UnknownFormatOption {
return nil, Context{}, errDoesNotProvide
}
return FromCatalog(catalog), Context{
Source: srcMetadata,
Distro: theDistro,
}, nil
}
func getSBOMReader(userInput string) (io.Reader, error) {
if userInput == "" {
// we only want to attempt reading in from stdin if the user has not specified other
// options from the CLI, otherwise we should not assume there is any valid input from stdin.
return stdinReader(), nil
}
if explicitlySpecifyingSBOM(userInput) {
filepath := strings.TrimPrefix(userInput, "sbom:")
sbom, err := openSbom(filepath)
if err != nil {
return nil, fmt.Errorf("unable to use specified SBOM: %w", err)
}
return sbom, nil
}
// as a last resort, see if the raw user input specified an SBOM file
if isPossibleSBOM(userInput) {
sbom, err := openSbom(userInput)
if err == nil {
return sbom, nil
}
}
// no usable SBOM is available
return nil, errDoesNotProvide
}
func stdinReader() io.Reader {
isPipedInput, err := internal.IsPipedInput()
if err != nil {
log.Warnf("unable to determine if there is piped input: %+v", err)
return nil
}
if !isPipedInput {
return nil
}
return os.Stdin
}
func openSbom(path string) (*os.File, error) {
expandedPath, err := homedir.Expand(path)
if err != nil {
return nil, fmt.Errorf("unable to open SBOM: %w", err)
}
sbom, err := os.Open(expandedPath)
if err != nil {
return nil, fmt.Errorf("unable to open SBOM: %w", err)
}
return sbom, nil
}
func isPossibleSBOM(userInput string) bool {
f, err := os.Open(userInput)
if err != nil {
return false
}
mType, err := mimetype.DetectReader(f)
if err != nil {
return false
}
if _, err := f.Seek(0, io.SeekStart); err != nil {
log.Warnf("unable to seek to the start of the possible SBOM file=%q: %w", userInput, err)
}
// we expect application/json, application/xml, and text/plain input documents. All of these are either
// text/plain or a descendant of text/plain. Anything else cannot be an input SBOM document.
return isAncestorOfMimetype(mType, "text/plain")
}
func isAncestorOfMimetype(mType *mimetype.MIME, expected string) bool {
for cur := mType; cur != nil; cur = cur.Parent() {
if cur.Is(expected) {
return true
}
}
return false
}
func explicitlySpecifyingSBOM(userInput string) bool {
return strings.HasPrefix(userInput, "sbom:")
}

View file

@ -1,7 +1,7 @@
package pkg
import (
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -134,12 +134,7 @@ func TestParseSyftJSON(t *testing.T) {
for _, test := range tests {
t.Run(test.Fixture, func(t *testing.T) {
fh, err := os.Open(test.Fixture)
if err != nil {
t.Fatalf("unable to open fixture: %+v", err)
}
pkgs, context, err := parseSyftJSON(fh)
pkgs, context, err := syftSBOMProvider(test.Fixture)
if err != nil {
t.Fatalf("unable to parse: %+v", err)
}
@ -148,6 +143,12 @@ func TestParseSyftJSON(t *testing.T) {
context.Source.ImageMetadata.RawManifest = nil
for _, d := range deep.Equal(test.Packages, pkgs) {
if strings.Contains(d, ".ID: ") {
// today ID's get assigned by the catalog, which will change in the future. But in the meantime
// that means that these IDs are random and should not be counted as a difference we care about in
// this test.
continue
}
t.Errorf("pkg diff: %s", d)
}
@ -159,13 +160,7 @@ func TestParseSyftJSON(t *testing.T) {
}
func TestParseSyftJSON_BadCPEs(t *testing.T) {
const testFixture = "test-fixtures/syft-java-bad-cpes.json"
fh, err := os.Open(testFixture)
if err != nil {
t.Fatalf("unable to open fixture: %+v", err)
}
pkgs, _, err := parseSyftJSON(fh)
pkgs, _, err := syftSBOMProvider("test-fixtures/syft-java-bad-cpes.json")
assert.NoError(t, err)
assert.Len(t, pkgs, 1)
}

View file

@ -12,5 +12,9 @@ func IsPipedInput() (bool, error) {
return false, fmt.Errorf("unable to determine if there is piped input: %w", err)
}
return fi.Mode()&os.ModeCharDevice == 0, nil
// note: we should NOT use the absence of a character device here as the hint that there may be input expected
// on stdin, as running grype as a subprocess you would expect no character device to be present but input can
// be from either stdin or indicated by the CLI. Checking if stdin is a pipe is the most direct way to determine
// if there *may* be bytes that will show up on stdin that should be used for the analysis source.
return fi.Mode()&os.ModeNamedPipe != 0, nil
}

View file

@ -0,0 +1,61 @@
package cli
import (
"fmt"
"path"
"strings"
"testing"
"time"
"github.com/anchore/stereoscope/pkg/imagetest"
)
func TestSubprocessStdin(t *testing.T) {
binDir := path.Dir(getGrypeSnapshotLocation(t, "linux"))
tests := []struct {
name string
args []string
env map[string]string
assertions []traitAssertion
}{
{
// regression
name: "ensure can be used by node subprocess (without hanging)",
args: []string{"-v", fmt.Sprintf("%s:%s:ro", binDir, "/app/bin"), imagetest.LoadFixtureImageIntoDocker(t, "image-node-subprocess"), "node", "/app.js"},
env: map[string]string{
"GRYPE_CHECK_FOR_APP_UPDATE": "false",
},
assertions: []traitAssertion{
assertSucceedingReturnCode,
},
},
{
// regression: https://github.com/anchore/grype/issues/267
name: "ensure can be used by java subprocess (without hanging)",
args: []string{"-v", fmt.Sprintf("%s:%s:ro", binDir, "/app/bin"), imagetest.LoadFixtureImageIntoDocker(t, "image-java-subprocess"), "java", "/app.java"},
env: map[string]string{
"GRYPE_CHECK_FOR_APP_UPDATE": "false",
},
assertions: []traitAssertion{
assertSucceedingReturnCode,
},
},
}
for _, test := range tests {
testFn := func(t *testing.T) {
cmd := getDockerRunCommand(t, test.args...)
stdout, stderr := runCommand(cmd, test.env)
for _, traitAssertionFn := range test.assertions {
traitAssertionFn(t, stdout, stderr, cmd.ProcessState.ExitCode())
}
if t.Failed() {
t.Log("STDOUT:\n", stdout)
t.Log("STDERR:\n", stderr)
t.Log("COMMAND:", strings.Join(cmd.Args, " "))
}
}
testWithTimeout(t, test.name, 60*time.Second, testFn)
}
}

View file

@ -0,0 +1,4 @@
FROM openjdk:15-slim-buster
COPY app.java /
ENV PATH="/app/bin:${PATH}"
WORKDIR /

View file

@ -0,0 +1,18 @@
import java.io.IOException;
public class GrypeExecutionTest {
public static void main(String[] args) {
try {
ProcessBuilder builder = new ProcessBuilder("grype", "registry:busybox:latest", "-vv");
builder.inheritIO();
Process process = builder.start();
process.waitFor();
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,4 @@
FROM node:16-stretch
COPY app.js /
ENV PATH="/app/bin:${PATH}"
WORKDIR /

View file

@ -0,0 +1,10 @@
require("child_process").spawn("grype", [
"-vv",
"registry:busybox:latest",
], {
// we want to see any output from stdout/stderr which is why they are inherited from the parent process.
// The real test is to make certain that piped input will not hang forever when nothing is provided on stdin
// and there is input from the user to not use stdin. That is --make certain that we don't use "stdin is a pipe"
// as the only indicator to expect analysis input from stdin.
stdio: ["pipe", "inherit", "inherit"]
});

View file

@ -25,3 +25,10 @@ func assertFailingReturnCode(tb testing.TB, _, _ string, rc int) {
tb.Errorf("expected a failure but got rc=%d", rc)
}
}
func assertSucceedingReturnCode(tb testing.TB, _, _ string, rc int) {
tb.Helper()
if rc != 0 {
tb.Errorf("expected a failure but got rc=%d", rc)
}
}

View file

@ -11,6 +11,7 @@ import (
"runtime"
"strings"
"testing"
"time"
"github.com/anchore/stereoscope/pkg/imagetest"
)
@ -25,24 +26,8 @@ func getFixtureImage(tb testing.TB, fixtureImageName string) string {
func getGrypeCommand(tb testing.TB, args ...string) *exec.Cmd {
tb.Helper()
var binaryLocation string
if os.Getenv("GRYPE_BINARY_LOCATION") != "" {
// GRYPE_BINARY_LOCATION is the absolute path to the snapshot binary
binaryLocation = os.Getenv("GRYPE_BINARY_LOCATION")
} else {
// note: there is a subtle - vs _ difference between these versions
switch runtime.GOOS {
case "darwin":
binaryLocation = path.Join(repoRoot(tb), fmt.Sprintf("snapshot/grype-macos_darwin_%s/grype", runtime.GOARCH))
case "linux":
binaryLocation = path.Join(repoRoot(tb), fmt.Sprintf("snapshot/grype_linux_%s/grype", runtime.GOARCH))
default:
tb.Fatalf("unsupported OS: %s", runtime.GOOS)
}
}
return exec.Command(
binaryLocation,
getGrypeSnapshotLocation(tb, runtime.GOOS),
append(
[]string{"-c", "../grype-test-config.yaml"},
args...,
@ -50,6 +35,36 @@ func getGrypeCommand(tb testing.TB, args ...string) *exec.Cmd {
)
}
func getGrypeSnapshotLocation(tb testing.TB, goOS string) string {
if os.Getenv("GRYPE_BINARY_LOCATION") != "" {
// GRYPE_BINARY_LOCATION is the absolute path to the snapshot binary
return os.Getenv("GRYPE_BINARY_LOCATION")
}
// note: there is a subtle - vs _ difference between these versions
switch goOS {
case "darwin":
return path.Join(repoRoot(tb), fmt.Sprintf("snapshot/grype-macos_darwin_%s/grype", runtime.GOARCH))
case "linux":
return path.Join(repoRoot(tb), fmt.Sprintf("snapshot/grype_linux_%s/grype", runtime.GOARCH))
default:
tb.Fatalf("unsupported OS: %s", runtime.GOOS)
}
return ""
}
func getDockerRunCommand(tb testing.TB, args ...string) *exec.Cmd {
tb.Helper()
return exec.Command(
"docker",
append(
[]string{"run"},
args...,
)...,
)
}
func runGrype(tb testing.TB, env map[string]string, args ...string) (*exec.Cmd, string, string) {
tb.Helper()
@ -134,3 +149,17 @@ func assertCommandExecutionSuccess(t testing.TB, cmd *exec.Cmd) {
t.Fatalf("unable to run command %q: %v", cmd, err)
}
}
func testWithTimeout(t *testing.T, name string, timeout time.Duration, test func(*testing.T)) {
done := make(chan bool)
go func() {
t.Run(name, test)
done <- true
}()
select {
case <-time.After(timeout):
t.Fatal("test timed out")
case <-done:
}
}