Add relationships for rust audit binary packages (#3500)

* add rust audit binary pkg relationships

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* fix linting

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2024-12-06 09:23:18 -05:00 committed by GitHub
parent 4adb56d2fe
commit 340b5e17f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 460 additions and 50 deletions

View File

@ -5,11 +5,11 @@ import (
"sort" "sort"
"strings" "strings"
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/scylladb/go-set/strset" "github.com/scylladb/go-set/strset"
"github.com/anchore/clio" "github.com/anchore/clio"
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/source/sourceproviders" "github.com/anchore/syft/syft/source/sourceproviders"
) )

View File

@ -5,11 +5,12 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"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/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/internal/windows" "github.com/anchore/syft/syft/internal/windows"
"github.com/wagoodman/go-progress"
) )
type fileIndexer struct { type fileIndexer struct {

View File

@ -1,13 +1,15 @@
package fileresolver package fileresolver
import ( import (
"github.com/anchore/stereoscope/pkg/file"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"io/fs" "io/fs"
"os" "os"
"path" "path"
"testing" "testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/file"
) )
// - Verify that both the parent and the path are indexed // - Verify that both the parent and the path are indexed

View File

@ -14,13 +14,14 @@ import (
"testing" "testing"
"time" "time"
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/file"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"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"
"go.uber.org/goleak" "go.uber.org/goleak"
stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/file"
) )
// Tests for filetree resolver when directory is used for index // Tests for filetree resolver when directory is used for index

View File

@ -9,6 +9,8 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/generic" "github.com/anchore/syft/syft/pkg/cataloger/generic"
) )
const cargoAuditBinaryCatalogerName = "cargo-auditable-binary-cataloger"
// NewCargoLockCataloger returns a new Rust Cargo lock file cataloger object. // NewCargoLockCataloger returns a new Rust Cargo lock file cataloger object.
func NewCargoLockCataloger() pkg.Cataloger { func NewCargoLockCataloger() pkg.Cataloger {
return generic.NewCataloger("rust-cargo-lock-cataloger"). return generic.NewCataloger("rust-cargo-lock-cataloger").
@ -18,6 +20,6 @@ func NewCargoLockCataloger() pkg.Cataloger {
// NewAuditBinaryCataloger returns a new Rust auditable binary cataloger object that can detect dependencies // NewAuditBinaryCataloger returns a new Rust auditable binary cataloger object that can detect dependencies
// in binaries produced with https://github.com/Shnatsel/rust-audit // in binaries produced with https://github.com/Shnatsel/rust-audit
func NewAuditBinaryCataloger() pkg.Cataloger { func NewAuditBinaryCataloger() pkg.Cataloger {
return generic.NewCataloger("cargo-auditable-binary-cataloger"). return generic.NewCataloger(cargoAuditBinaryCatalogerName).
WithParserByMimeTypes(parseAuditBinary, mimetype.ExecutableMIMETypeSet.List()...) WithParserByMimeTypes(parseAuditBinary, mimetype.ExecutableMIMETypeSet.List()...)
} }

View File

@ -3,48 +3,270 @@ package rust
import ( import (
"testing" "testing"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
) )
func TestNewAuditBinaryCataloger(t *testing.T) { func TestNewAuditBinaryCataloger(t *testing.T) {
locations := file.NewLocationSet(file.NewVirtualLocation("/usr/local/bin/hello_world", "/usr/local/bin/hello_world"))
argh := pkg.Package{
Name: "argh",
Version: "0.1.12",
PURL: "pkg:cargo/argh@0.1.12",
FoundBy: "cargo-auditable-binary-cataloger",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
Metadata: pkg.RustBinaryAuditEntry{
Name: "argh",
Version: "0.1.12",
Source: "crates.io",
},
}
arghDerive := pkg.Package{
Name: "argh_derive",
Version: "0.1.12",
PURL: "pkg:cargo/argh_derive@0.1.12",
FoundBy: "cargo-auditable-binary-cataloger",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
Metadata: pkg.RustBinaryAuditEntry{
Name: "argh_derive",
Version: "0.1.12",
Source: "crates.io",
},
}
arghShared := pkg.Package{
Name: "argh_shared",
Version: "0.1.12",
PURL: "pkg:cargo/argh_shared@0.1.12",
FoundBy: "cargo-auditable-binary-cataloger",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
Metadata: pkg.RustBinaryAuditEntry{
Name: "argh_shared",
Version: "0.1.12",
Source: "crates.io",
},
}
helloWorld := pkg.Package{
Name: "hello_world",
Version: "0.1.0",
PURL: "pkg:cargo/hello_world@0.1.0",
FoundBy: "cargo-auditable-binary-cataloger",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
Metadata: pkg.RustBinaryAuditEntry{
Name: "hello_world",
Version: "0.1.0",
Source: "local",
},
}
procMacro2 := pkg.Package{
Name: "proc-macro2",
Version: "1.0.92",
PURL: "pkg:cargo/proc-macro2@1.0.92",
FoundBy: "cargo-auditable-binary-cataloger",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
Metadata: pkg.RustBinaryAuditEntry{
Name: "proc-macro2",
Version: "1.0.92",
Source: "crates.io",
},
}
quote := pkg.Package{
Name: "quote",
Version: "1.0.37",
PURL: "pkg:cargo/quote@1.0.37",
FoundBy: "cargo-auditable-binary-cataloger",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
Metadata: pkg.RustBinaryAuditEntry{
Name: "quote",
Version: "1.0.37",
Source: "crates.io",
},
}
serde := pkg.Package{
Name: "serde",
Version: "1.0.215",
PURL: "pkg:cargo/serde@1.0.215",
FoundBy: "cargo-auditable-binary-cataloger",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
Metadata: pkg.RustBinaryAuditEntry{
Name: "serde",
Version: "1.0.215",
Source: "crates.io",
},
}
serdeDerive := pkg.Package{
Name: "serde_derive",
Version: "1.0.215",
PURL: "pkg:cargo/serde_derive@1.0.215",
FoundBy: "cargo-auditable-binary-cataloger",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
Metadata: pkg.RustBinaryAuditEntry{
Name: "serde_derive",
Version: "1.0.215",
Source: "crates.io",
},
}
syn := pkg.Package{
Name: "syn",
Version: "2.0.90",
PURL: "pkg:cargo/syn@2.0.90",
FoundBy: "cargo-auditable-binary-cataloger",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
Metadata: pkg.RustBinaryAuditEntry{
Name: "syn",
Version: "2.0.90",
Source: "crates.io",
},
}
unicodeIdent := pkg.Package{
Name: "unicode-ident",
Version: "1.0.14",
PURL: "pkg:cargo/unicode-ident@1.0.14",
FoundBy: "cargo-auditable-binary-cataloger",
Locations: locations,
Language: pkg.Rust,
Type: pkg.RustPkg,
Metadata: pkg.RustBinaryAuditEntry{
Name: "unicode-ident",
Version: "1.0.14",
Source: "crates.io",
},
}
expectedPkgs := []pkg.Package{ expectedPkgs := []pkg.Package{
argh,
arghDerive,
arghShared,
helloWorld,
procMacro2,
quote,
serde,
serdeDerive,
syn,
unicodeIdent,
}
expectedRelationships := []artifact.Relationship{
{ {
Name: "auditable", From: argh,
Version: "0.1.0", To: helloWorld,
PURL: "pkg:cargo/auditable@0.1.0", Type: artifact.DependencyOfRelationship,
FoundBy: "cargo-auditable-binary-cataloger",
Locations: file.NewLocationSet(file.NewVirtualLocation("/hello-auditable", "/hello-auditable")),
Language: pkg.Rust,
Type: pkg.RustPkg,
Metadata: pkg.RustBinaryAuditEntry{
Name: "auditable",
Version: "0.1.0",
Source: "local",
},
}, },
{ {
Name: "hello-auditable", From: arghDerive,
Version: "0.1.0", To: argh,
PURL: "pkg:cargo/hello-auditable@0.1.0", Type: artifact.DependencyOfRelationship,
FoundBy: "cargo-auditable-binary-cataloger", },
Locations: file.NewLocationSet(file.NewVirtualLocation("/hello-auditable", "/hello-auditable")), {
Language: pkg.Rust, From: arghShared,
Type: pkg.RustPkg, To: argh,
Metadata: pkg.RustBinaryAuditEntry{ Type: artifact.DependencyOfRelationship,
Name: "hello-auditable", },
Version: "0.1.0", {
Source: "local", From: arghShared,
}, To: arghDerive,
Type: artifact.DependencyOfRelationship,
},
{
From: procMacro2,
To: arghDerive,
Type: artifact.DependencyOfRelationship,
},
{
From: procMacro2,
To: quote,
Type: artifact.DependencyOfRelationship,
},
{
From: procMacro2,
To: serdeDerive,
Type: artifact.DependencyOfRelationship,
},
{
From: procMacro2,
To: syn,
Type: artifact.DependencyOfRelationship,
},
{
From: quote,
To: arghDerive,
Type: artifact.DependencyOfRelationship,
},
{
From: quote,
To: serdeDerive,
Type: artifact.DependencyOfRelationship,
},
{
From: quote,
To: syn,
Type: artifact.DependencyOfRelationship,
},
{
From: serde,
To: arghShared,
Type: artifact.DependencyOfRelationship,
},
{
From: serdeDerive,
To: serde,
Type: artifact.DependencyOfRelationship,
},
{
From: syn,
To: arghDerive,
Type: artifact.DependencyOfRelationship,
},
{
From: syn,
To: serdeDerive,
Type: artifact.DependencyOfRelationship,
},
{
From: unicodeIdent,
To: procMacro2,
Type: artifact.DependencyOfRelationship,
},
{
From: unicodeIdent,
To: syn,
Type: artifact.DependencyOfRelationship,
}, },
} }
pkgtest.NewCatalogTester(). pkgtest.NewCatalogTester().
WithImageResolver(t, "image-audit"). WithImageResolver(t, "image-audit").
IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change
Expects(expectedPkgs, nil). Expects(expectedPkgs, expectedRelationships).
TestCataloger(t, NewAuditBinaryCataloger()) TestCataloger(t, NewAuditBinaryCataloger())
} }

View File

@ -25,19 +25,6 @@ func newPackageFromCargoMetadata(m pkg.RustCargoLockEntry, locations ...file.Loc
return p return p
} }
func newPackagesFromAudit(location file.Location, versionInfo rustaudit.VersionInfo) []pkg.Package {
var pkgs []pkg.Package
for _, dep := range versionInfo.Packages {
p := newPackageFromAudit(&dep, location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
if pkg.IsValid(&p) && dep.Kind == rustaudit.Runtime {
pkgs = append(pkgs, p)
}
}
return pkgs
}
func newPackageFromAudit(dep *rustaudit.Package, locations ...file.Location) pkg.Package { func newPackageFromAudit(dep *rustaudit.Package, locations ...file.Location) pkg.Package {
p := pkg.Package{ p := pkg.Package{
Name: dep.Name, Name: dep.Name,
@ -46,6 +33,7 @@ func newPackageFromAudit(dep *rustaudit.Package, locations ...file.Location) pkg
Language: pkg.Rust, Language: pkg.Rust,
Type: pkg.RustPkg, Type: pkg.RustPkg,
Locations: file.NewLocationSet(locations...), Locations: file.NewLocationSet(locations...),
FoundBy: cargoAuditBinaryCatalogerName,
Metadata: pkg.RustBinaryAuditEntry{ Metadata: pkg.RustBinaryAuditEntry{
Name: dep.Name, Name: dep.Name,
Version: dep.Version, Version: dep.Version,

View File

@ -5,9 +5,10 @@ import (
"errors" "errors"
"fmt" "fmt"
rustaudit "github.com/microsoft/go-rustaudit" "github.com/microsoft/go-rustaudit"
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/relationship"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader" "github.com/anchore/syft/syft/internal/unionreader"
@ -18,6 +19,7 @@ import (
// Catalog identifies executables then attempts to read Rust dependency information from them // Catalog identifies executables then attempts to read Rust dependency information from them
func parseAuditBinary(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { func parseAuditBinary(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package var pkgs []pkg.Package
var relationships []artifact.Relationship
unionReader, err := unionreader.GetUnionReader(reader.ReadCloser) unionReader, err := unionreader.GetUnionReader(reader.ReadCloser)
if err != nil { if err != nil {
@ -26,10 +28,12 @@ func parseAuditBinary(_ context.Context, _ file.Resolver, _ *generic.Environment
infos, err := parseAuditBinaryEntry(unionReader, reader.RealPath) infos, err := parseAuditBinaryEntry(unionReader, reader.RealPath)
for _, versionInfo := range infos { for _, versionInfo := range infos {
pkgs = append(pkgs, newPackagesFromAudit(reader.Location, versionInfo)...) auditPkgs, auditRelationships := processAuditVersionInfo(reader.Location, versionInfo)
pkgs = append(pkgs, auditPkgs...)
relationships = append(relationships, auditRelationships...)
} }
return pkgs, nil, err return pkgs, relationships, err
} }
// scanFile scans file to try to report the Rust crate dependencies // scanFile scans file to try to report the Rust crate dependencies
@ -61,3 +65,64 @@ func parseAuditBinaryEntry(reader unionreader.UnionReader, filename string) ([]r
return versionInfos, nil return versionInfos, nil
} }
// auditPkgPair is a helper struct to track the original index of the package in the original audit report + the syft package created for it
type auditPkgPair struct {
pkg *pkg.Package
rustPkg rustaudit.Package
index int
}
func processAuditVersionInfo(location file.Location, versionInfo rustaudit.VersionInfo) ([]pkg.Package, []artifact.Relationship) {
var pkgs []pkg.Package
// first pass: create packages for all runtime dependencies (skip dev and invalid dependencies)
pairsByOgIndex := make(map[int]auditPkgPair)
for idx, dep := range versionInfo.Packages {
p := newPackageFromAudit(&dep, location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
pair := auditPkgPair{
rustPkg: dep,
index: idx,
}
if pkg.IsValid(&p) && dep.Kind == rustaudit.Runtime {
pkgs = append(pkgs, p)
pair.pkg = &pkgs[len(pkgs)-1]
}
pairsByOgIndex[idx] = pair
}
// second pass: create relationships between any packages created
// we have all the original audit package indices + info, but not all audit packages will have syft packages.
// we need to be careful to not create relationships for packages that were not created.
var rels []artifact.Relationship
for _, parentPair := range pairsByOgIndex {
// the rust-audit report lists dependencies by index from the original version info object. We need to find
// the syft packages created for each listed dependency from that original object.
for _, ogPkgIndex := range parentPair.rustPkg.Dependencies {
if ogPkgIndex >= uint(len(versionInfo.Packages)) {
log.WithFields("pkg", parentPair.pkg).Trace("cargo audit dependency index out of range: %d", ogPkgIndex)
continue
}
depPair, ok := pairsByOgIndex[int(ogPkgIndex)]
if !ok {
log.WithFields("pkg", parentPair.pkg).Trace("cargo audit dependency not found: %d", ogPkgIndex)
continue
}
if depPair.pkg == nil || parentPair.pkg == nil {
// skip relationships for syft packages that were not created from the original report (no matter the reason)
continue
}
rels = append(rels, artifact.Relationship{
From: *depPair.pkg,
To: *parentPair.pkg,
Type: artifact.DependencyOfRelationship,
})
}
}
relationship.Sort(rels)
return pkgs, rels
}

View File

@ -0,0 +1,96 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "argh"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7af5ba06967ff7214ce4c7419c7d185be7ecd6cc4965a8f6e1d8ce0398aad219"
dependencies = [
"argh_derive",
"argh_shared",
]
[[package]]
name = "argh_derive"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56df0aeedf6b7a2fc67d06db35b09684c3e8da0c95f8f27685cb17e08413d87a"
dependencies = [
"argh_shared",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "argh_shared"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5693f39141bda5760ecc4111ab08da40565d1771038c4a0250f03457ec707531"
dependencies = [
"serde",
]
[[package]]
name = "hello_world"
version = "0.1.0"
dependencies = [
"argh",
]
[[package]]
name = "proc-macro2"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"

View File

@ -0,0 +1,7 @@
[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"
[dependencies]
argh = "0.1"

View File

@ -1 +1,14 @@
FROM docker.io/tofay/hello-rust-auditable:latest@sha256:1d35d1e007180b3f7500aae5e27560697909132ca9a6d480c4c825534c1c47a9 FROM rust:1.74.0 AS builder
WORKDIR /app
RUN cargo install cargo-auditable --locked
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo fetch
RUN cargo auditable build --release
FROM scratch
COPY --from=builder /app/target/release/hello_world /usr/local/bin/hello_world

View File

@ -0,0 +1,13 @@
use argh::FromArgs;
#[derive(FromArgs)]
#[argh(description = "A simple Hello World CLI application.")]
struct Args {
#[argh(option, description = "name to greet")]
name: String,
}
fn main() {
let args: Args = argh::from_env();
println!("Hello, {}!", args.name);
}