diff --git a/go.mod b/go.mod index 68e28f445..d4629f12a 100644 --- a/go.mod +++ b/go.mod @@ -76,6 +76,12 @@ require ( modernc.org/sqlite v1.28.0 ) +require ( + github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be + github.com/charmbracelet/bubbles v0.16.1 + github.com/jedib0t/go-pretty/v6 v6.4.9 +) + require ( dario.cat/mergo v1.0.0 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect @@ -91,9 +97,9 @@ require ( github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/becheran/wildmatch-go v1.0.0 // indirect - github.com/charmbracelet/bubbles v0.16.1 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect github.com/containerd/cgroups v1.1.0 // indirect @@ -179,6 +185,7 @@ require ( github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/sagikazarmark/locafero v0.3.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sahilm/fuzzy v0.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.1 // indirect diff --git a/go.sum b/go.sum index a0a5878ab..109384eef 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,8 @@ github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= @@ -462,6 +464,8 @@ github.com/invopop/jsonschema v0.7.0 h1:2vgQcBz1n256N+FpX3Jq7Y17AjYt46Ig3zIWyy77 github.com/invopop/jsonschema v0.7.0/go.mod h1:O9uiLokuu0+MGFlyiaqtWxwqJm41/+8Nj0lD7A36YH0= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jedib0t/go-pretty/v6 v6.4.9 h1:vZ6bjGg2eBSrJn365qlxGcaWu09Id+LHtrfDWlB2Usc= +github.com/jedib0t/go-pretty/v6 v6.4.9/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs= github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -501,6 +505,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -531,6 +537,7 @@ github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZ github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= @@ -627,6 +634,7 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= @@ -671,6 +679,8 @@ github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9c github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= +github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= @@ -736,6 +746,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= diff --git a/syft/pkg/cataloger/binary/cataloger_test.go b/syft/pkg/cataloger/binary/cataloger_test.go index be0e2f9a9..f576d5d5c 100644 --- a/syft/pkg/cataloger/binary/cataloger_test.go +++ b/syft/pkg/cataloger/binary/cataloger_test.go @@ -2,7 +2,9 @@ package binary import ( "errors" + "flag" "fmt" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/testutil" "io" "strings" "testing" @@ -18,15 +20,16 @@ import ( "github.com/anchore/syft/syft/source" ) +var mustUseOriginalBinaries = flag.Bool("must-use-original-binaries", false, "force the use of binaries for testing (instead of snippets)") + func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { tests := []struct { - name string - fixtureDir string - expected pkg.Package + name string + logicalFixture string + expected pkg.Package }{ { - name: "positive-postgresql-15beta4", - fixtureDir: "test-fixtures/classifiers/positive/postgresql-15beta4", + logicalFixture: "postgres/15beta4/linux-amd64", expected: pkg.Package{ Name: "postgresql", Version: "15beta4", @@ -37,8 +40,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-postgresql-15.1", - fixtureDir: "test-fixtures/classifiers/positive/postgresql-15.1", + logicalFixture: "postgres/15.1/linux-amd64", expected: pkg.Package{ Name: "postgresql", Version: "15.1", @@ -49,8 +51,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-postgresql-9.6.24", - fixtureDir: "test-fixtures/classifiers/positive/postgresql-9.6.24", + logicalFixture: "postgres/9.6.24/linux-amd64", expected: pkg.Package{ Name: "postgresql", Version: "9.6.24", @@ -60,21 +61,20 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { Metadata: metadata("postgresql-binary"), }, }, + // TODO: missing original binary + //{ + // logicalFixture: "postgres/9.5alpha1/linux-amd64", + // expected: pkg.Package{ + // Name: "postgresql", + // Version: "9.5alpha1", + // Type: "binary", + // PURL: "pkg:generic/postgresql@9.5alpha1", + // Locations: locations("postgres"), + // Metadata: metadata("postgresql-binary"), + // }, + //}, { - name: "positive-postgresql-9.5alpha1", - fixtureDir: "test-fixtures/classifiers/positive/postgresql-9.5alpha1", - expected: pkg.Package{ - Name: "postgresql", - Version: "9.5alpha1", - Type: "binary", - PURL: "pkg:generic/postgresql@9.5alpha1", - Locations: locations("postgres"), - Metadata: metadata("postgresql-binary"), - }, - }, - { - name: "positive-mysql-8.0.34", - fixtureDir: "test-fixtures/classifiers/positive/mysql-8.0.34", + logicalFixture: "mysql/8.0.34/linux-amd64", expected: pkg.Package{ Name: "mysql", Version: "8.0.34", @@ -85,8 +85,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-mysql-5.6.51", - fixtureDir: "test-fixtures/classifiers/positive/mysql-5.6.51", + logicalFixture: "mysql/5.6.51/linux-amd64", expected: pkg.Package{ Name: "mysql", Version: "5.6.51", @@ -97,8 +96,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-mariadb-10.6.15", - fixtureDir: "test-fixtures/classifiers/positive/mariadb-10.6.15", + logicalFixture: "mariadb/10.6.15/linux-amd64", expected: pkg.Package{ Name: "mariadb", Version: "10.6.15", @@ -108,45 +106,42 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { Metadata: metadata("mariadb-binary"), }, }, + // TODO: need to add original binary + //{ + // logicalFixture: "traefik/2.9.6/linux-amd64", + // expected: pkg.Package{ + // Name: "traefik", + // Version: "2.9.6", + // Type: "binary", + // PURL: "pkg:generic/traefik@2.9.6", + // Locations: locations("traefik"), + // Metadata: metadata("traefik-binary"), + // }, + //}, + //{ + // logicalFixture: "test-fixtures/classifiers/positive/traefik-1.7.34/linux-amd64", + // expected: pkg.Package{ + // Name: "traefik", + // Version: "1.7.34", + // Type: "binary", + // PURL: "pkg:generic/traefik@1.7.34", + // Locations: locations("traefik"), + // Metadata: metadata("traefik-binary"), + // }, + //}, { - name: "positive-traefik-2.9.6", - fixtureDir: "test-fixtures/classifiers/positive/traefik-2.9.6", - expected: pkg.Package{ - Name: "traefik", - Version: "2.9.6", - Type: "binary", - PURL: "pkg:generic/traefik@2.9.6", - Locations: locations("traefik"), - Metadata: metadata("traefik-binary"), - }, - }, - { - name: "positive-traefik-1.7.34", - fixtureDir: "test-fixtures/classifiers/positive/traefik-1.7.34", - expected: pkg.Package{ - Name: "traefik", - Version: "1.7.34", - Type: "binary", - PURL: "pkg:generic/traefik@1.7.34", - Locations: locations("traefik"), - Metadata: metadata("traefik-binary"), - }, - }, - { - name: "positive-memcached-1.6.18", - fixtureDir: "test-fixtures/classifiers/positive/memcached-1.6.18", + logicalFixture: "memcached/1.6.18/linux-amd64", expected: pkg.Package{ Name: "memcached", Version: "1.6.18", Type: "binary", PURL: "pkg:generic/memcached@1.6.18", - Locations: locations("memcached-1.6.18-176467412e0722ae1b2c1159555d33574653ebfa87e8591d88c1e6e416ab3019-19-100/memcached"), + Locations: locations("memcached"), Metadata: metadata("memcached-binary"), }, }, { - name: "positive-httpd-2.4.54", - fixtureDir: "test-fixtures/classifiers/positive/httpd-2.4.54", + logicalFixture: "httpd/2.4.54/linux-amd64", expected: pkg.Package{ Name: "httpd", Version: "2.4.54", @@ -156,81 +151,80 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { Metadata: metadata("httpd-binary"), }, }, + + // TODO: missing original binaries + //{ + // logicalFixture: "test-fixtures/classifiers/positive/php-cli-8.2.1/linux-amd64", + // expected: pkg.Package{ + // Name: "php-cli", + // Version: "8.2.1", + // Type: "binary", + // PURL: "pkg:generic/php-cli@8.2.1", + // Locations: locations("php"), + // Metadata: metadata("php-cli-binary"), + // }, + //}, + //{ + // logicalFixture: "test-fixtures/classifiers/positive/php-fpm-8.2.1/linux-amd64", + // expected: pkg.Package{ + // Name: "php-fpm", + // Version: "8.2.1", + // Type: "binary", + // PURL: "pkg:generic/php-fpm@8.2.1", + // Locations: locations("php-fpm"), + // Metadata: metadata("php-fpm-binary"), + // }, + //}, + //{ + // logicalFixture: "test-fixtures/classifiers/positive/php-apache-8.2.1/linux-amd64", + // expected: pkg.Package{ + // Name: "libphp", + // Version: "8.2.1", + // Type: "binary", + // PURL: "pkg:generic/php@8.2.1", + // Locations: locations("libphp.so"), + // Metadata: metadata("php-apache-binary"), + // }, + //}, + + // TODO: original binary is different than test fixture + //{ + // logicalFixture: "perl/5.12.5/linux-amd64", + // expected: pkg.Package{ + // Name: "perl", + // Version: "5.12.5", + // Type: "binary", + // PURL: "pkg:generic/perl@5.12.5", + // Locations: locations("perl"), + // Metadata: metadata("perl-binary"), + // }, + //}, + //{ + // name: "positive-perl-5.20.0", + // logicalFixture: "test-fixtures/classifiers/positive/perl-5.20.0/linux-amd64", + // expected: pkg.Package{ + // Name: "perl", + // Version: "5.20.0", + // Type: "binary", + // PURL: "pkg:generic/perl@5.20.0", + // Locations: locations("perl"), + // Metadata: metadata("perl-binary"), + // }, + //}, + //{ + // name: "positive-perl-5.37.8", + // logicalFixture: "test-fixtures/classifiers/positive/perl-5.37.8/linux-amd64", + // expected: pkg.Package{ + // Name: "perl", + // Version: "5.37.8", + // Type: "binary", + // PURL: "pkg:generic/perl@5.37.8", + // Locations: locations("perl"), + // Metadata: metadata("perl-binary"), + // }, + //}, { - name: "positive-php-cli-8.2.1", - fixtureDir: "test-fixtures/classifiers/positive/php-cli-8.2.1", - expected: pkg.Package{ - Name: "php-cli", - Version: "8.2.1", - Type: "binary", - PURL: "pkg:generic/php-cli@8.2.1", - Locations: locations("php"), - Metadata: metadata("php-cli-binary"), - }, - }, - { - name: "positive-php-fpm-8.2.1", - fixtureDir: "test-fixtures/classifiers/positive/php-fpm-8.2.1", - expected: pkg.Package{ - Name: "php-fpm", - Version: "8.2.1", - Type: "binary", - PURL: "pkg:generic/php-fpm@8.2.1", - Locations: locations("php-fpm"), - Metadata: metadata("php-fpm-binary"), - }, - }, - { - name: "positive-php-apache-8.2.1", - fixtureDir: "test-fixtures/classifiers/positive/php-apache-8.2.1", - expected: pkg.Package{ - Name: "libphp", - Version: "8.2.1", - Type: "binary", - PURL: "pkg:generic/php@8.2.1", - Locations: locations("libphp.so"), - Metadata: metadata("php-apache-binary"), - }, - }, - { - name: "positive-perl-5.12.5", - fixtureDir: "test-fixtures/classifiers/positive/perl-5.12.5", - expected: pkg.Package{ - Name: "perl", - Version: "5.12.5", - Type: "binary", - PURL: "pkg:generic/perl@5.12.5", - Locations: locations("perl"), - Metadata: metadata("perl-binary"), - }, - }, - { - name: "positive-perl-5.20.0", - fixtureDir: "test-fixtures/classifiers/positive/perl-5.20.0", - expected: pkg.Package{ - Name: "perl", - Version: "5.20.0", - Type: "binary", - PURL: "pkg:generic/perl@5.20.0", - Locations: locations("perl"), - Metadata: metadata("perl-binary"), - }, - }, - { - name: "positive-perl-5.37.8", - fixtureDir: "test-fixtures/classifiers/positive/perl-5.37.8", - expected: pkg.Package{ - Name: "perl", - Version: "5.37.8", - Type: "binary", - PURL: "pkg:generic/perl@5.37.8", - Locations: locations("perl"), - Metadata: metadata("perl-binary"), - }, - }, - { - name: "positive-haproxy-1.5.14", - fixtureDir: "test-fixtures/classifiers/positive/haproxy-1.5.14-5ff6ed7c0149382701391f268e78793a623d6b8fd2b9b65fe89c08b6041fd4d4-527350-100", + logicalFixture: "haproxy/1.5.14/linux-amd64", expected: pkg.Package{ Name: "haproxy", Version: "1.5.14", @@ -241,8 +235,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-haproxy-1.8.22", - fixtureDir: "test-fixtures/classifiers/positive/haproxy-1.8.22-cc32c297e83f2626d253d8615008c199a1dc9cba13883731f78ecbe9bfc524d4-1386742-100", + logicalFixture: "haproxy/1.8.22/linux-amd64", expected: pkg.Package{ Name: "haproxy", Version: "1.8.22", @@ -253,8 +246,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-haproxy-2.7.3", - fixtureDir: "test-fixtures/classifiers/positive/haproxy-2.7.3-99d01769ac3457cf33b7e3eb93050453e20c09afe5631f810fd658fb5314bbd3-3156502-100", + logicalFixture: "haproxy/2.7.3/linux-amd64", expected: pkg.Package{ Name: "haproxy", Version: "2.7.3", @@ -264,21 +256,9 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { Metadata: metadata("haproxy-binary"), }, }, + { - name: "positive-redis-2.8.23", - fixtureDir: "test-fixtures/classifiers/positive/redis-server-2.8.23", - expected: pkg.Package{ - Name: "redis", - Version: "2.8.23", - Type: "binary", - PURL: "pkg:generic/redis@2.8.23", - Locations: locations("redis-server"), - Metadata: metadata("redis-binary"), - }, - }, - { - name: "positive-helm-3.11.1", - fixtureDir: "test-fixtures/classifiers/dynamic/helm-3.11.1", + logicalFixture: "helm/3.11.1/linux-amd64", expected: pkg.Package{ Name: "helm", Version: "3.11.1", @@ -289,8 +269,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-helm-3.10.3", - fixtureDir: "test-fixtures/classifiers/dynamic/helm-3.10.3", + logicalFixture: "helm/3.10.3/linux-amd64", expected: pkg.Package{ Name: "helm", Version: "3.10.3", @@ -300,112 +279,129 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { Metadata: metadata("helm"), }, }, + + // TODO: missing original binaries + //{ + // logicalFixture: "test-fixtures/classifiers/positive/redis-server-2.8.23/linux-amd64", + // expected: pkg.Package{ + // Name: "redis", + // Version: "2.8.23", + // Type: "binary", + // PURL: "pkg:generic/redis@2.8.23", + // Locations: locations("redis-server"), + // Metadata: metadata("redis-binary"), + // }, + //}, + + //{ + // name: "positive-redis-4.0.11", + // logicalFixture: "test-fixtures/classifiers/positive/redis-server-4.0.11/linux-amd64", + // expected: pkg.Package{ + // Name: "redis", + // Version: "4.0.11", + // Type: "binary", + // PURL: "pkg:generic/redis@4.0.11", + // Locations: locations("redis-server"), + // Metadata: metadata("redis-binary"), + // }, + //}, + //{ + // name: "positive-redis-5.0.0", + // logicalFixture: "test-fixtures/classifiers/positive/redis-server-5.0.0/linux-amd64", + // expected: pkg.Package{ + // Name: "redis", + // Version: "5.0.0", + // Type: "binary", + // PURL: "pkg:generic/redis@5.0.0", + // Locations: locations("redis-server"), + // Metadata: metadata("redis-binary"), + // }, + //}, + //{ + // name: "positive-redis-6.0.16", + // logicalFixture: "test-fixtures/classifiers/positive/redis-server-6.0.16", + // expected: pkg.Package{ + // Name: "redis", + // Version: "6.0.16", + // Type: "binary", + // PURL: "pkg:generic/redis@6.0.16", + // Locations: locations("redis-server"), + // Metadata: metadata("redis-binary"), + // }, + //}, + //{ + // name: "positive-redis-7.0.0", + // logicalFixture: "test-fixtures/classifiers/positive/redis-server-7.0.0", + // expected: pkg.Package{ + // Name: "redis", + // Version: "7.0.0", + // Type: "binary", + // PURL: "pkg:generic/redis@7.0.0", + // Locations: locations("redis-server"), + // Metadata: metadata("redis-binary"), + // }, + //}, + //{ + // name: "positive-redis-7.0.14", + // logicalFixture: "test-fixtures/classifiers/positive/redis-server-7.0.14", + // expected: pkg.Package{ + // Name: "redis", + // Version: "7.0.14", + // Type: "binary", + // PURL: "pkg:generic/redis@7.0.14", + // Locations: locations("redis-server"), + // Metadata: metadata("redis-binary"), + // }, + //}, + //{ + // name: "positive-redis-7.2.3-amd64", + // logicalFixture: "test-fixtures/classifiers/positive/redis-server-7.2.3-amd64", + // expected: pkg.Package{ + // Name: "redis", + // Version: "7.2.3", + // Type: "binary", + // PURL: "pkg:generic/redis@7.2.3", + // Locations: locations("redis-server"), + // Metadata: metadata("redis-binary"), + // }, + //}, + //{ + // name: "positive-redis-7.2.3-arm64", + // logicalFixture: "test-fixtures/classifiers/positive/redis-server-7.2.3-arm64", + // expected: pkg.Package{ + // Name: "redis", + // Version: "7.2.3", + // Type: "binary", + // PURL: "pkg:generic/redis@7.2.3", + // Locations: locations("redis-server"), + // Metadata: metadata("redis-binary"), + // }, + //}, + + // TODO: missing original binaries + //{ + // name: "positive-libpython3.7.so", + // logicalFixture: "test-fixtures/classifiers/positive/python-binary-lib-3.7", + // expected: pkg.Package{ + // Name: "python", + // Version: "3.7.4", + // PURL: "pkg:generic/python@3.7.4", + // Locations: locations("libpython3.7.so"), + // Metadata: metadata("python-binary-lib"), + // }, + //}, + { - name: "positive-redis-4.0.11", - fixtureDir: "test-fixtures/classifiers/positive/redis-server-4.0.11", - expected: pkg.Package{ - Name: "redis", - Version: "4.0.11", - Type: "binary", - PURL: "pkg:generic/redis@4.0.11", - Locations: locations("redis-server"), - Metadata: metadata("redis-binary"), - }, - }, - { - name: "positive-redis-5.0.0", - fixtureDir: "test-fixtures/classifiers/positive/redis-server-5.0.0", - expected: pkg.Package{ - Name: "redis", - Version: "5.0.0", - Type: "binary", - PURL: "pkg:generic/redis@5.0.0", - Locations: locations("redis-server"), - Metadata: metadata("redis-binary"), - }, - }, - { - name: "positive-redis-6.0.16", - fixtureDir: "test-fixtures/classifiers/positive/redis-server-6.0.16", - expected: pkg.Package{ - Name: "redis", - Version: "6.0.16", - Type: "binary", - PURL: "pkg:generic/redis@6.0.16", - Locations: locations("redis-server"), - Metadata: metadata("redis-binary"), - }, - }, - { - name: "positive-redis-7.0.0", - fixtureDir: "test-fixtures/classifiers/positive/redis-server-7.0.0", - expected: pkg.Package{ - Name: "redis", - Version: "7.0.0", - Type: "binary", - PURL: "pkg:generic/redis@7.0.0", - Locations: locations("redis-server"), - Metadata: metadata("redis-binary"), - }, - }, - { - name: "positive-redis-7.0.14", - fixtureDir: "test-fixtures/classifiers/positive/redis-server-7.0.14", - expected: pkg.Package{ - Name: "redis", - Version: "7.0.14", - Type: "binary", - PURL: "pkg:generic/redis@7.0.14", - Locations: locations("redis-server"), - Metadata: metadata("redis-binary"), - }, - }, - { - name: "positive-redis-7.2.3-amd64", - fixtureDir: "test-fixtures/classifiers/positive/redis-server-7.2.3-amd64", - expected: pkg.Package{ - Name: "redis", - Version: "7.2.3", - Type: "binary", - PURL: "pkg:generic/redis@7.2.3", - Locations: locations("redis-server"), - Metadata: metadata("redis-binary"), - }, - }, - { - name: "positive-redis-7.2.3-arm64", - fixtureDir: "test-fixtures/classifiers/positive/redis-server-7.2.3-arm64", - expected: pkg.Package{ - Name: "redis", - Version: "7.2.3", - Type: "binary", - PURL: "pkg:generic/redis@7.2.3", - Locations: locations("redis-server"), - Metadata: metadata("redis-binary"), - }, - }, - { - name: "positive-libpython3.7.so", - fixtureDir: "test-fixtures/classifiers/positive/python-binary-lib-3.7", - expected: pkg.Package{ - Name: "python", - Version: "3.7.4", - PURL: "pkg:generic/python@3.7.4", - Locations: locations("libpython3.7.so"), - Metadata: metadata("python-binary-lib"), - }, - }, - { - name: "positive-python-3.11.2-from-shared-lib", - fixtureDir: "test-fixtures/classifiers/dynamic/python-binary-shared-lib-3.11", + // note: dynamic (non-snippet) test case + logicalFixture: "python-slim-shared-libs/3.11/linux-amd64", expected: pkg.Package{ Name: "python", Version: "3.11.2", PURL: "pkg:generic/python@3.11.2", - Locations: locations("python3", "libpython3.11.so.1.0"), + Locations: locations("python3.11", "libpython3.11.so.1.0"), Metadata: pkg.BinarySignature{ Matches: []pkg.ClassifierMatch{ - match("python-binary", "python3"), + match("python-binary", "python3.11"), match("python-binary", "libpython3.11.so.1.0"), match("python-binary-lib", "libpython3.11.so.1.0"), }, @@ -413,8 +409,8 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-python-3.9-from-shared-redhat-lib", - fixtureDir: "test-fixtures/classifiers/dynamic/python-binary-shared-lib-redhat-3.9", + // note: dynamic (non-snippet) test case + logicalFixture: "python-rhel-shared-libs/3.9/linux-amd64", expected: pkg.Package{ Name: "python", Version: "3.9.13", @@ -430,8 +426,8 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-python-binary-with-version-3.9", - fixtureDir: "test-fixtures/classifiers/dynamic/python-binary-with-version-3.9", + // note: dynamic (non-snippet) test case + logicalFixture: "python3.9/3.9.16/linux-amd64", expected: pkg.Package{ Name: "python", Version: "3.9.2", @@ -445,8 +441,8 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-python-binary-3.4-alpine", - fixtureDir: "test-fixtures/classifiers/dynamic/python-binary-3.4-alpine", + // note: dynamic (non-snippet) test case + logicalFixture: "python-alpine-shared-libs/3.4/linux-amd64", expected: pkg.Package{ Name: "python", Version: "3.4.10", @@ -461,188 +457,196 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, }, + // TODO: missing original binary + //{ + // name: "positive-python-3.5-with-incorrect-match", + // logicalFixture: "test-fixtures/classifiers/positive/python-3.5-with-incorrect-match", + // expected: pkg.Package{ + // Name: "python", + // Version: "3.5.3", + // PURL: "pkg:generic/python@3.5.3", + // Locations: locations("python3.5"), + // Metadata: metadata("python-binary"), + // }, + //}, + + // TODO: can't seem to get this lined up with the new binary pulled down... the original snippet also has content I can't account for + //{ + // logicalFixture: "python/3.6/linux-amd64", + // expected: pkg.Package{ + // Name: "python", + // Version: "3.6.3", + // PURL: "pkg:generic/python@3.6.3", + // Locations: locations("python3.6"), + // Metadata: metadata("python-binary"), + // }, + //}, + + // TODO: missing original binary + //{ + // name: "positive-python-duplicates", + // logicalFixture: "test-fixtures/classifiers/positive/python-duplicates", + // expected: pkg.Package{ + // Name: "python", + // Version: "3.8.16", + // Type: "binary", + // PURL: "pkg:generic/python@3.8.16", + // Locations: locations("dir/python3.8", "python3.8", "libpython3.8.so"), + // Metadata: pkg.BinarySignature{ + // Matches: []pkg.ClassifierMatch{ + // match("python-binary", "dir/python3.8"), + // match("python-binary", "python3.8"), + // match("python-binary-lib", "libpython3.8.so"), + // }, + // }, + // }, + //}, + { - name: "positive-python-3.5-with-incorrect-match", - fixtureDir: "test-fixtures/classifiers/positive/python-3.5-with-incorrect-match", - expected: pkg.Package{ - Name: "python", - Version: "3.5.3", - PURL: "pkg:generic/python@3.5.3", - Locations: locations("python3.5"), - Metadata: metadata("python-binary"), - }, - }, - { - name: "positive-python3.6", - fixtureDir: "test-fixtures/classifiers/positive/python-binary-3.6", - expected: pkg.Package{ - Name: "python", - Version: "3.6.3", - PURL: "pkg:generic/python@3.6.3", - Locations: locations("python3.6"), - Metadata: metadata("python-binary"), - }, - }, - { - name: "positive-python-duplicates", - fixtureDir: "test-fixtures/classifiers/positive/python-duplicates", - expected: pkg.Package{ - Name: "python", - Version: "3.8.16", - Type: "binary", - PURL: "pkg:generic/python@3.8.16", - Locations: locations("dir/python3.8", "python3.8", "libpython3.8.so"), - Metadata: pkg.BinarySignature{ - Matches: []pkg.ClassifierMatch{ - match("python-binary", "dir/python3.8"), - match("python-binary", "python3.8"), - match("python-binary-lib", "libpython3.8.so"), - }, - }, - }, - }, - { - name: "positive-go", - fixtureDir: "test-fixtures/classifiers/positive/go-1.21.3-6a05bd57efff25a9d682b8e25ce572ee7ff723f4f5bcbba6429012121e247365-7879390-100", + logicalFixture: "go/1.21.3/linux-amd64", expected: pkg.Package{ Name: "go", Version: "1.21.3", PURL: "pkg:generic/go@1.21.3", - Locations: locations("go-1.21.3-6a05bd57efff25a9d682b8e25ce572ee7ff723f4f5bcbba6429012121e247365-7879390-100/go"), + Locations: locations("go"), Metadata: metadata("go-binary"), }, }, { - name: "positive-node", - fixtureDir: "test-fixtures/classifiers/positive/node-19.2.1", + logicalFixture: "node/19.2.0/linux-amd64", expected: pkg.Package{ Name: "node", - Version: "19.2.1", - PURL: "pkg:generic/node@19.2.1", + Version: "19.2.0", + PURL: "pkg:generic/node@19.2.0", Locations: locations("node"), Metadata: metadata("nodejs-binary"), }, }, + // TODO: captured outside of the original binary + //{ + // name: "positive-go-hint", + // logicalFixture: "test-fixtures/classifiers/positive/go-hint-1.15", + // expected: pkg.Package{ + // Name: "go", + // Version: "1.15", + // PURL: "pkg:generic/go@1.15", + // Locations: locations("VERSION"), + // Metadata: metadata("go-binary-hint"), + // }, + //}, { - name: "positive-go-hint", - fixtureDir: "test-fixtures/classifiers/positive/go-hint-1.15", - expected: pkg.Package{ - Name: "go", - Version: "1.15", - PURL: "pkg:generic/go@1.15", - Locations: locations("VERSION"), - Metadata: metadata("go-binary-hint"), - }, - }, - { - name: "positive-busybox", - fixtureDir: "test-fixtures/classifiers/positive/busybox-3.33.3", + // note: this is testing BUSYBOX which is typically through a link to "[" (in this case a symlink but in + // practice this is often a hard link). + logicalFixture: `busybox/1.36.1/linux-amd64`, expected: pkg.Package{ Name: "busybox", - Version: "3.33.3", + Version: "1.36.1", Locations: locations("["), // note: busybox is a link to [ Metadata: metadata("busybox-binary", "[", "busybox"), }, }, + // TODO: need to get the original binaries + //{ + // logicalFixture: "openjdk/1.8.0", + // expected: pkg.Package{ + // Name: "java", + // Version: "1.8.0_352-b08", + // Type: "binary", + // PURL: "pkg:generic/java@1.8.0_352-b08", + // Locations: locations("java"), + // Metadata: metadata("java-binary-openjdk", "java"), + // }, + //}, + //{ + // name: "positive-java-openjdk-lts", + // logicalFixture: "test-fixtures/classifiers/positive/openjdk-lts-11.0.17", + // expected: pkg.Package{ + // Name: "java", + // Version: "11.0.17+8-LTS", + // Type: "binary", + // PURL: "pkg:generic/java@11.0.17+8-LTS", + // Locations: locations("java"), + // Metadata: metadata("java-binary-openjdk", "java"), + // }, + //}, + //{ + // name: "positive-java-oracle", + // logicalFixture: "test-fixtures/classifiers/positive/oracle-java-19.0.1", + // expected: pkg.Package{ + // Name: "java", + // Version: "19.0.1+10-21", + // Type: "binary", + // PURL: "pkg:generic/java@19.0.1+10-21", + // Locations: locations("java"), + // Metadata: metadata("java-binary-oracle", "java"), + // }, + //}, + //{ + // name: "positive-java-oracle-macos", + // logicalFixture: "test-fixtures/classifiers/positive/oracle-macos-java-19.0.1", + // expected: pkg.Package{ + // Name: "java", + // Version: "19.0.1+10-21", + // Type: "binary", + // PURL: "pkg:generic/java@19.0.1+10-21", + // Locations: locations("java"), + // Metadata: metadata("java-binary-oracle", "java"), + // }, + //}, { - name: "positive-java-openjdk", - fixtureDir: "test-fixtures/classifiers/positive/openjdk-1.8.0", + name: "positive-java-ibm", + logicalFixture: "java-jre-ibm/1.8.0_391/linux-amd64", expected: pkg.Package{ Name: "java", - Version: "1.8.0_352-b08", + Version: "1.8.0-foreman_2023_10_12_13_27-b00", Type: "binary", - PURL: "pkg:generic/java@1.8.0_352-b08", - Locations: locations("java"), - Metadata: metadata("java-binary-openjdk", "java"), - }, - }, - { - name: "positive-java-openjdk-lts", - fixtureDir: "test-fixtures/classifiers/positive/openjdk-lts-11.0.17", - expected: pkg.Package{ - Name: "java", - Version: "11.0.17+8-LTS", - Type: "binary", - PURL: "pkg:generic/java@11.0.17+8-LTS", - Locations: locations("java"), - Metadata: metadata("java-binary-openjdk", "java"), - }, - }, - { - name: "positive-java-oracle", - fixtureDir: "test-fixtures/classifiers/positive/oracle-java-19.0.1", - expected: pkg.Package{ - Name: "java", - Version: "19.0.1+10-21", - Type: "binary", - PURL: "pkg:generic/java@19.0.1+10-21", - Locations: locations("java"), - Metadata: metadata("java-binary-oracle", "java"), - }, - }, - { - name: "positive-java-oracle-macos", - fixtureDir: "test-fixtures/classifiers/positive/oracle-macos-java-19.0.1", - expected: pkg.Package{ - Name: "java", - Version: "19.0.1+10-21", - Type: "binary", - PURL: "pkg:generic/java@19.0.1+10-21", - Locations: locations("java"), - Metadata: metadata("java-binary-oracle", "java"), - }, - }, - { - name: "positive-java-ibm", - fixtureDir: "test-fixtures/classifiers/positive/ibm-java-1.8.0", - expected: pkg.Package{ - Name: "java", - Version: "1.8.0-foreman_2022_09_22_15_30-b00", - Type: "binary", - PURL: "pkg:generic/java@1.8.0-foreman_2022_09_22_15_30-b00", + PURL: "pkg:generic/java@1.8.0-foreman_2023_10_12_13_27-b00", Locations: locations("java"), Metadata: metadata("java-binary-ibm", "java"), }, }, + // TODO: need to get the original binaries + //{ + // name: "positive-rust-1.50.0-macos", + // logicalFixture: "test-fixtures/classifiers/positive/rust-1.50.0", + // expected: pkg.Package{ + // Name: "rust", + // Version: "1.50.0", + // Type: "binary", + // PURL: "pkg:generic/rust@1.50.0", + // Locations: locations("lib/rustlib/aarch64-apple-darwin/lib/libstd-f6f9eec1635e636a.dylib"), + // Metadata: metadata("rust-standard-library-macos"), + // }, + //}, + //{ + // name: "positive-rust-1.67.1-macos", + // logicalFixture: "test-fixtures/classifiers/positive/rust-1.67.1/toolchains/stable-aarch64-apple-darwin", + // expected: pkg.Package{ + // Name: "rust", + // Version: "1.67.1", + // Type: "binary", + // PURL: "pkg:generic/rust@1.67.1", + // Locations: locations("lib/libstd-16f2b65e77054c42.dylib"), + // Metadata: metadata("rust-standard-library-macos"), + // }, + //}, + //{ + // name: "positive-rust-1.67.1-linux", + // logicalFixture: "test-fixtures/classifiers/positive/rust-1.67.1/toolchains/stable-x86_64-unknown-linux-musl", + // expected: pkg.Package{ + // Name: "rust", + // Version: "1.67.1", + // Type: "binary", + // PURL: "pkg:generic/rust@1.67.1", + // Locations: locations("lib/libstd-86aefecbddda356d.so"), + // Metadata: metadata("rust-standard-library-linux"), + // }, + //}, { - name: "positive-rust-1.50.0-macos", - fixtureDir: "test-fixtures/classifiers/positive/rust-1.50.0", - expected: pkg.Package{ - Name: "rust", - Version: "1.50.0", - Type: "binary", - PURL: "pkg:generic/rust@1.50.0", - Locations: locations("lib/rustlib/aarch64-apple-darwin/lib/libstd-f6f9eec1635e636a.dylib"), - Metadata: metadata("rust-standard-library-macos"), - }, - }, - { - name: "positive-rust-1.67.1-macos", - fixtureDir: "test-fixtures/classifiers/positive/rust-1.67.1/toolchains/stable-aarch64-apple-darwin", - expected: pkg.Package{ - Name: "rust", - Version: "1.67.1", - Type: "binary", - PURL: "pkg:generic/rust@1.67.1", - Locations: locations("lib/libstd-16f2b65e77054c42.dylib"), - Metadata: metadata("rust-standard-library-macos"), - }, - }, - { - name: "positive-rust-1.67.1-linux", - fixtureDir: "test-fixtures/classifiers/positive/rust-1.67.1/toolchains/stable-x86_64-unknown-linux-musl", - expected: pkg.Package{ - Name: "rust", - Version: "1.67.1", - Type: "binary", - PURL: "pkg:generic/rust@1.67.1", - Locations: locations("lib/libstd-86aefecbddda356d.so"), - Metadata: metadata("rust-standard-library-linux"), - }, - }, - { - name: "positive-ruby-3.2.1", - fixtureDir: "test-fixtures/classifiers/dynamic/ruby-library-3.2.1", + // note: dynamic (non-snippet) test case + + name: "positive-ruby-3.2.1", + logicalFixture: "ruby-bullseye-shared-libs/3.2.1/linux-amd64", expected: pkg.Package{ Name: "ruby", Version: "3.2.1", @@ -658,8 +662,8 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-ruby-2.7.7", - fixtureDir: "test-fixtures/classifiers/dynamic/ruby-library-2.7.7", + // note: dynamic (non-snippet) test case + logicalFixture: "ruby-bullseye-shared-libs/2.7.7/linux-amd64", expected: pkg.Package{ Name: "ruby", Version: "2.7.7p221", @@ -675,8 +679,8 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-ruby-2.6.10", - fixtureDir: "test-fixtures/classifiers/dynamic/ruby-library-2.6.10", + // note: dynamic (non-snippet) test case + logicalFixture: "ruby-shared-libs/2.6.10/linux-amd64", expected: pkg.Package{ Name: "ruby", Version: "2.6.10p210", @@ -691,21 +695,20 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, }, + //{ + // name: "positive-ruby-1.9.3p551", + // logicalFixture: "test-fixtures/classifiers/positive/ruby-1.9.3p551/linux-amd64", + // expected: pkg.Package{ + // Name: "ruby", + // Version: "1.9.3p551", + // Type: "binary", + // PURL: "pkg:generic/ruby@1.9.3p551", + // Locations: locations("ruby"), + // Metadata: metadata("ruby-binary"), + // }, + //}, { - name: "positive-ruby-1.9.3p551", - fixtureDir: "test-fixtures/classifiers/positive/ruby-1.9.3p551", - expected: pkg.Package{ - Name: "ruby", - Version: "1.9.3p551", - Type: "binary", - PURL: "pkg:generic/ruby@1.9.3p551", - Locations: locations("ruby"), - Metadata: metadata("ruby-binary"), - }, - }, - { - name: "positive-consul-1.15.2", - fixtureDir: "test-fixtures/classifiers/dynamic/consul-1.15.2", + logicalFixture: "consul/1.15.2/linux-amd64", expected: pkg.Package{ Name: "consul", Version: "1.15.2", @@ -716,20 +719,29 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-erlang-25.3.2.7", - fixtureDir: "test-fixtures/classifiers/positive/erlexec-25.3.2.7-2224b3107551c409fb8da6d85eaa1ed730d088db438924c97d6e25851381dd28-91724-100", + logicalFixture: "erlexec/25.3.2.6/linux-amd64", expected: pkg.Package{ Name: "erlang", - Version: "25.3.2.7", + Version: "25.3.2.6", Type: "binary", - PURL: "pkg:generic/erlang@25.3.2.7", + PURL: "pkg:generic/erlang@25.3.2.6", Locations: locations("erlexec"), Metadata: metadata("erlang-binary"), }, }, { - name: "positive-nginx-1.25.1", - fixtureDir: "test-fixtures/classifiers/positive/nginx-1.25.1", + logicalFixture: "erlexec/26.2.0.0/linux-amd64", + expected: pkg.Package{ + Name: "erlang", + Version: "26.2", + Type: "binary", + PURL: "pkg:generic/erlang@26.2", + Locations: locations("erlexec"), + Metadata: metadata("erlang-binary"), + }, + }, + { + logicalFixture: "nginx/1.25.1/linux-amd64", expected: pkg.Package{ Name: "nginx", Version: "1.25.1", @@ -740,8 +752,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-nginx-openresty-1.21.4.2", - fixtureDir: "test-fixtures/classifiers/positive/nginx-openresty-1.21.4.2", + logicalFixture: "nginx-openresty/1.21.4.3/linux-amd64", expected: pkg.Package{ Name: "nginx", Version: "1.21.4", @@ -752,8 +763,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { }, }, { - name: "positive-bash-5.1.16", - fixtureDir: "test-fixtures/classifiers/positive/bash-5.1.16-2c336c63e26881d2f02f34379024e7c314bce572c08cbaa319bacbbec29f93ed-1210190-100", + logicalFixture: "bash/5.1.16/linux-amd64", expected: pkg.Package{ Name: "bash", Version: "5.1.16", @@ -766,10 +776,12 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { } for _, test := range tests { - t.Run(test.name, func(t *testing.T) { + t.Run(test.logicalFixture, func(t *testing.T) { c := NewCataloger() - src, err := source.NewFromDirectoryPath(test.fixtureDir) + path := testutil.SnippetOrBinary(t, test.logicalFixture, *mustUseOriginalBinaries) + + src, err := source.NewFromDirectoryPath(path) require.NoError(t, err) resolver, err := src.FileResolver(source.SquashedScope) @@ -778,7 +790,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) { packages, _, err := c.Catalog(resolver) require.NoError(t, err) - require.Len(t, packages, 1) + require.Len(t, packages, 1, "mismatched package count") assertPackagesAreEqual(t, test.expected, packages[0]) }) diff --git a/syft/pkg/cataloger/binary/default_classifiers.go b/syft/pkg/cataloger/binary/default_classifiers.go index 681233e41..34f35e90b 100644 --- a/syft/pkg/cataloger/binary/default_classifiers.go +++ b/syft/pkg/cataloger/binary/default_classifiers.go @@ -233,8 +233,6 @@ var defaultClassifiers = []classifier{ Class: "mysql-binary", FileGlob: "**/mysql", EvidenceMatcher: fileContentsVersionMatcher( - // ../../mysql-8.0.34 - // /mysql-5.6.51/bld/client `(?m).*/mysql-(?P[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)`), Package: "mysql", PURL: mustPURL("pkg:generic/mysql@version"), @@ -286,9 +284,15 @@ var defaultClassifiers = []classifier{ { Class: "erlang-binary", FileGlob: "**/erlexec", - EvidenceMatcher: fileContentsVersionMatcher( - // [NUL]/usr/local/src/otp-25.3.2.7/erts/ - `(?m)/usr/local/src/otp-(?P[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+?)/erts/`, + EvidenceMatcher: evidenceMatchers( + fileContentsVersionMatcher( + // [NUL]/usr/src/otp_src_25.3.2.6/erts/ + `(?m)/src/otp_src_(?P[0-9]+\.[0-9]+(\.[0-9]+\.[0-9]+)?)/erts/`, + ), + fileContentsVersionMatcher( + // [NUL]/usr/local/src/otp-25.3.2.7/erts/ + `(?m)/usr/local/src/otp-(?P[0-9]+\.[0-9]+(\.[0-9]+\.[0-9]+)?)/erts/`, + ), ), Package: "erlang", PURL: mustPURL("pkg:generic/erlang@version"), diff --git a/syft/pkg/cataloger/binary/test-fixtures/.gitignore b/syft/pkg/cataloger/binary/test-fixtures/.gitignore index fd45a0e70..9a442ff1e 100644 --- a/syft/pkg/cataloger/binary/test-fixtures/.gitignore +++ b/syft/pkg/cataloger/binary/test-fixtures/.gitignore @@ -1 +1,2 @@ classifiers/dynamic +classifiers/bin \ No newline at end of file diff --git a/syft/pkg/cataloger/binary/test-fixtures/Makefile b/syft/pkg/cataloger/binary/test-fixtures/Makefile index 4ed523068..2eaf95f44 100644 --- a/syft/pkg/cataloger/binary/test-fixtures/Makefile +++ b/syft/pkg/cataloger/binary/test-fixtures/Makefile @@ -1,110 +1,24 @@ -.PHONY: all -all: \ - classifiers/dynamic/python-binary-shared-lib-3.11 \ - classifiers/dynamic/python-binary-shared-lib-redhat-3.9 \ - classifiers/dynamic/python-binary-with-version-3.9 \ - classifiers/dynamic/python-binary-3.4-alpine \ - classifiers/dynamic/ruby-library-3.2.1 \ - classifiers/dynamic/ruby-library-2.7.7 \ - classifiers/dynamic/ruby-library-2.6.10 \ - classifiers/dynamic/helm-3.11.1 \ - classifiers/dynamic/helm-3.10.3 \ - classifiers/dynamic/consul-1.15.2 +.PHONY: default list download download-all fingerprint + +.DEFAULT_GOAL := default + +default: download + +list: ## list all managed binaries and snippets + go run ./manager list + +download: ## download only binaries that are not covered by a snippet + go run ./manager download $(name) --skip-if-covered-by-snippet + +download-all: ## download all managed binaries + go run ./manager download + +fingerprint: ## prints the sha256sum of the any input to the download command (to determine if there is a cache miss) + @cat ./config.yaml | sha256sum | awk '{print $$1}' +## Halp! ################################# -classifiers/dynamic/python-binary-shared-lib-3.11: - $(eval $@_image := "python:3.11-slim@sha256:0b106e1d2bf485c2a41474bc9cd5103e9eea4e179f40f10741b53b127059221e") - ./get-image-file.sh $($@_image) \ - /usr/local/bin/python3.11 \ - $@/python3 - ./get-image-file.sh $($@_image) \ - /usr/local/lib/libpython3.11.so.1.0 \ - $@/libpython3.11.so.1.0 - -classifiers/dynamic/python-binary-shared-lib-redhat-3.9: - $(eval $@_image := "registry.access.redhat.com/ubi8/python-39@sha256:f3cf958b96ce016b63e3e163e488f52e42891304dafef5a0811563f22e3cbad0") - ./get-image-file.sh $($@_image) \ - /usr/bin/python3.9 \ - $@/python3.9 - ./get-image-file.sh $($@_image) \ - /usr/lib64/libpython3.9.so.1.0 \ - $@/libpython3.9.so.1.0 - -classifiers/dynamic/python-binary-with-version-3.9: - $(eval $@_image := "python:3.9.16-bullseye@sha256:93fb93c461a2e47a2176706fad1f39eaacd5dd40e19c0b018699a28c03eb2e2a") - ./get-image-file.sh $($@_image) \ - /usr/bin/python3.9 \ - $@/python3.9 - -classifiers/dynamic/python-binary-3.4-alpine: - $(eval $@_image := "python:3.4-alpine@sha256:c210b660e2ea553a7afa23b41a6ed112f85dbce25cbcb567c75dfe05342a4c4b") - ./get-image-file.sh $($@_image) \ - /usr/local/bin/python3.4 \ - $@/python3.4 - ./get-image-file.sh $($@_image) \ - /usr/local/lib/libpython3.4m.so.1.0 \ - $@/libpython3.4m.so.1.0 - -classifiers/dynamic/ruby-library-3.2.1: - $(eval $@_image := "ruby:3.2.1-bullseye@sha256:b4a140656b0c5d26c0a80559b228b4d343f3fdbf56682fcbe88f6db1fa9afa6b") - ./get-image-file.sh $($@_image) \ - /usr/local/bin/ruby \ - $@/ruby - ./get-image-file.sh $($@_image) \ - /usr/local/lib/libruby.so.3.2.1 \ - $@/libruby.so.3.2.1 - ./get-image-file.sh $($@_image) \ - /usr/local/lib/libruby.so.3.2 \ - $@/libruby.so.3.2 - -classifiers/dynamic/ruby-library-2.7.7: - $(eval $@_image := "ruby:2.7.7-bullseye@sha256:055191740a063f33fef1f09423e5ed8f91143aae62a3772a90910118464c5120") - ./get-image-file.sh $($@_image) \ - /usr/local/bin/ruby \ - $@/ruby - ./get-image-file.sh $($@_image) \ - /usr/local/lib/libruby.so.2.7.7 \ - $@/libruby.so.2.7.7 - ./get-image-file.sh $($@_image) \ - /usr/local/lib/libruby.so.2.7 \ - $@/libruby.so.2.7 - -classifiers/dynamic/ruby-library-2.6.10: - $(eval $@_image := "ruby:2.6.10@sha256:771a810704167e55da8a19970c5dfa6eb795dfee32547adffa29ea72703f7243") - ./get-image-file.sh $($@_image) \ - /usr/local/bin/ruby \ - $@/ruby - ./get-image-file.sh $($@_image) \ - /usr/local/lib/libruby.so.2.6.10 \ - $@/libruby.so.2.6.10 - ./get-image-file.sh $($@_image) \ - /usr/local/lib/libruby.so.2.6 \ - $@/libruby.so.2.6 - -classifiers/dynamic/helm-3.11.1: - $(eval $@_image := "alpine/helm:3.11.1@sha256:8628e3695fb743a8b9de89626f1b7a221280c2152c0e288c2504e59b68233e8b") - ./get-image-file.sh $($@_image) \ - /usr/bin/helm \ - $@/helm - -classifiers/dynamic/helm-3.10.3: - $(eval $@_image := "argoproj/argocd:v2.6.4@sha256:61fcbba187ff53c00696cb580edf70cada59c45cf399d8477631acf43cf522ee") - ./get-image-file.sh $($@_image) \ - /usr/local/bin/helm \ - $@/helm - -classifiers/dynamic/consul-1.15.2: - $(eval $@_image := "hashicorp/consul:1.15.2@sha256:c2169f3bb18dd947ae8eb5f6766896695c71fb439f050a3343e0007d895615b8") - ./get-image-file.sh $($@_image) \ - /bin/consul \ - $@/consul - -.PHONY: clean -clean: - rm -rf classifiers/dynamic - -.PHONY: cache.fingerprint -cache.fingerprint: # for CI - $(title,Install test fixture fingerprint) - @find ./classifiers/dynamic/* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee cache.fingerprint >> cache.fingerprint +.PHONY: help +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}' \ No newline at end of file diff --git a/syft/pkg/cataloger/binary/test-fixtures/README.md b/syft/pkg/cataloger/binary/test-fixtures/README.md new file mode 100644 index 000000000..8b7d42a03 --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/README.md @@ -0,0 +1,78 @@ +# Binary cataloger test fixtures + +To test the binary cataloger we run it against a set of files ("test fixtures"). There are two kinds of test fixtures: + +- **Full binaries**: files downloaded and cached at test runtime +- **Snippets**: ~100 byte files checked into the repo + +The upside with snippets is that they live with the test, don't necessarily require network access or hosting concerns, and are easy to add. The downside is that they are not the entire real binary so modifications may require recreating the snippet entirely. + +The upside with full binaries is that they are the "Real McCoy" and allows the business logic to change without needing to update the fixture. The downside is that they require network access and take up a lot of space. For instance, downloading all binaries for testing today requires downloading ~15GB of container images and ends up being ~500MB of disk space. + +You can find the test fixtures at the following locations: +``` +syft/pkg/cataloger/binary/test-fixtures/ +└── classifiers/ + ├── bin/ # full binaries + ├── ... + └── snippets/ # snippets +``` + +And use tooling to list and manage the fixtures: + +- `make list` - list all fixtures +- `make download` - download binaries that are not covered by a snippet +- `make download-all` - download all binaries +- `go run ./manager add-snippet` - add a new snippet based off of a configured binary +- `capture-snippet.sh` - add a new snippet based off of a binary on your local machine (not recommended, but allowed) + +There is a `config.yaml` that tracks all binaries that the tests can use. This makes it possible to download it at any time from a hosted source. Today the only method allowed is to download a container image and extract files out. + +## Testing + +The test cases have been setup to allow testing against full binaries or a mix of both (default). +To force running only against full binaries run with: + +```bash +go test -must-use-full-binaries ./syft/pkg/cataloger/binary/test-fixtures/... +``` + +## Adding a new test fixture + +### Adding a full binary + +1. Add a new entry to `config.yaml` with the following fields + - if you are adding a single binary, the `name` field does not need to be specified + - the `name` field is useful for distinguishing a quality about the binary (e.g. `java` vs `java-jre-ibm`) + +2. Run `make download` and ensure your new binary is downloaded + + +### Adding a snippet + +Even if you are adding a snippet, it is best practice to: + +- create that snippet from a full binary (not craft a snippet by hand) +- track where the binary is from and how to download it in `config.yaml` + +1. Follow the steps above to [add a full binary](#adding-a-full-binary) + +2. Run `go run ./manager add-snippet` and follow the prompts to create a new snippet + - you should see your binary in the list of binaries to choose from. If not, check step 2 + - if the search results in no matching snippets, you can specify your own search with `--search-for ` + - you should see a new snippet file created in `snippets/` + +3. Write a test that references your new snippet by `//` + - `` is the name of the binary (e.g. `curl`) or the name in `config.yaml` if specified + - note that your test does not know about if it's running against a snippet or a full binary + +### Adding a custom snippet + +If you need to add a snippet that is not based off of a full binary, you can use the `capture-snippet.sh` script. + +```bash +./capture-snippet.sh [--search-for ] [--length ] [--prefix-length ] [--group ] +``` + + +This is **not** recommended because it is not reproducible and does not allow for the test to be run against a full binary. \ No newline at end of file diff --git a/syft/pkg/cataloger/binary/test-fixtures/get-fixture-snippet.sh b/syft/pkg/cataloger/binary/test-fixtures/capture-snippet.sh similarity index 76% rename from syft/pkg/cataloger/binary/test-fixtures/get-fixture-snippet.sh rename to syft/pkg/cataloger/binary/test-fixtures/capture-snippet.sh index 9d99eaa64..c154b804a 100755 --- a/syft/pkg/cataloger/binary/test-fixtures/get-fixture-snippet.sh +++ b/syft/pkg/cataloger/binary/test-fixtures/capture-snippet.sh @@ -4,10 +4,11 @@ LENGTH=100 PREFIX_LENGTH=10 SEARCH_FOR='' +GROUP_NAME='' # Function to show usage usage() { - echo "Usage: $0 [--search-for ] [--length ] [--prefix-length ]" + echo "Usage: $0 [--search-for ] [--length ] [--prefix-length ] [--group ]" exit 1 } @@ -26,6 +27,11 @@ while [[ $# -gt 0 ]]; do shift # past argument shift # past value ;; + --group) + GROUP_NAME="$2" + shift # past argument + shift # past value + ;; --prefix-length) PREFIX_LENGTH="$2" shift # past argument @@ -50,6 +56,11 @@ if [ -z "$BINARY_FILE" ] || [ -z "$VERSION" ]; then usage fi +# if group name is empty use the binary filename +if [ -z "$GROUP_NAME" ]; then + GROUP_NAME=$(basename "$BINARY_FILE") +fi + # check if xxd is even installed if ! command -v xxd &> /dev/null; then echo "xxd not found. Please install xxd." @@ -59,7 +70,7 @@ fi PATTERN=${SEARCH_FOR:-$VERSION} -PATTERN_RESULTS=$(strings -a -t d "$BINARY_FILE" | grep "$PATTERN") +PATTERN_RESULTS=$(strings -t d "$BINARY_FILE" | grep "$PATTERN") # if there are multiple matches, prompt the user to select one if [ $(echo "$PATTERN_RESULTS" | wc -l) -gt 1 ]; then @@ -116,27 +127,4 @@ if [ "$RESPONSE" != "y" ]; then exit 1 fi -# generate a text file with metadata and the binary snippet -SHA256=$(sha256sum "$BINARY_FILE" | cut -d ' ' -f 1) -DATE=$(date) -BASE64_PATTERN=$(echo -n "$PATTERN" | base64) -FILENAME=$(basename "$BINARY_FILE") -INFO=$(file -b "$BINARY_FILE") -OUTPUT_DIRECTORY="classifiers/positive/$FILENAME-$VERSION" -mkdir "$OUTPUT_DIRECTORY" - -OUTPUT_FILE="$OUTPUT_DIRECTORY/$FILENAME" - -cat > "$OUTPUT_FILE" <> "$OUTPUT_FILE" - -echo "Snippet written to $OUTPUT_FILE" +go run ./manager write-snippet "$BINARY_FILE" --offset "$OFFSET" --length "$LENGTH" --name "$GROUP_NAME" --version "$VERSION" diff --git a/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/bash/5.1.16/linux-amd64/bash b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/bash/5.1.16/linux-amd64/bash new file mode 100644 index 000000000..ff22b2145 Binary files /dev/null and b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/bash/5.1.16/linux-amd64/bash differ diff --git a/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/busybox/1.36.1/linux-amd64/[ b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/busybox/1.36.1/linux-amd64/[ new file mode 100644 index 000000000..9d8fa0a2d Binary files /dev/null and b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/busybox/1.36.1/linux-amd64/[ differ diff --git a/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/busybox/1.36.1/linux-amd64/busybox b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/busybox/1.36.1/linux-amd64/busybox new file mode 120000 index 000000000..c3e3150b8 --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/busybox/1.36.1/linux-amd64/busybox @@ -0,0 +1 @@ +./[ \ No newline at end of file diff --git a/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/consul/1.15.2/linux-amd64/consul b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/consul/1.15.2/linux-amd64/consul new file mode 100644 index 000000000..1612ea9bf --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/classifiers/snippets/consul/1.15.2/linux-amd64/consul @@ -0,0 +1,12 @@ +name: consul +offset: 57272433 +length: 100 +snippetSha256: a4a955b180d6df471797a9f17f5ebf6f23b92d688d40532712683922e119dac0 +fileSha256: 5c5fed218247eaf43c3b54008b6a4c5d5cfa1b38539d6e7bfc09ac04623389fc + +### byte snippet to follow ### +e%7D" /> + + + ] [--version ] + +// capture snippet from a binary identified by offset +// manager capture snippet --binary --offset --length + +func New() (*cobra.Command, error) { + cfgP, err := config.Read() + if err != nil { + return nil, err + } + + cfg := *cfgP + + root := commands.Root(cfg) + + root.AddCommand( + commands.List(cfg), + commands.Download(cfg), + commands.AddSnippet(cfg), + commands.WriteSnippet(cfg), + ) + + return root, nil +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/add_snippet.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/add_snippet.go new file mode 100644 index 000000000..0abf3d49a --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/add_snippet.go @@ -0,0 +1,108 @@ +package commands + +import ( + "fmt" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui" + "github.com/anmitsu/go-shlex" + "github.com/spf13/cobra" + "os" + "os/exec" + "strings" +) + +func AddSnippet(appConfig config.Application) *cobra.Command { + var binaryPath, searchPattern string + var length, prefixLength int + + cmd := &cobra.Command{ + Use: "add-snippet", + Short: "capture snippets from binaries", + Args: cobra.NoArgs, + PreRunE: func(cmd *cobra.Command, args []string) error { + + candidates, err := internal.ListAllBinaries(appConfig) + if err != nil { + return fmt.Errorf("unable to list binaries: %w", err) + } + + // launch the UI to pick a path + var binaryPaths []string + for _, k := range internal.NewLogicalEntryKeys(candidates) { + info := candidates[k] + if info.BinaryPath != "" { + binaryPaths = append(binaryPaths, info.BinaryPath) + } + } + + binaryPath, err = ui.PromptSelectBinary(binaryPaths) + if err != nil { + return err + } + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + name, version, _, err := inferInfoFromBinaryPath(appConfig, binaryPath) + if err != nil { + return fmt.Errorf("unable to infer name and version from binary path: %w", err) + } + + if searchPattern == "" { + searchPattern = strings.ReplaceAll(version, ".", `\\.`) + } + + return runAddSnippet(binaryPath, name, version, searchPattern, length, prefixLength) + }, + } + + cmd.Flags().StringVar(&searchPattern, "search-for", "", "the pattern to search for in the binary (defaults to the version)") + cmd.Flags().IntVar(&length, "length", 100, "the length of the snippet to capture") + cmd.Flags().IntVar(&prefixLength, "prefix-length", 10, "number of bytes before the search hit to capture") + + return cmd +} + +func runAddSnippet(binaryPath, name, version, searchPattern string, length, prefixLength int) error { + // invoke ./capture-snippet.sh [--search-for ] [--length ] [--prefix-length ]" + + cmd := exec.Command("./capture-snippet.sh", binaryPath, version) + + var args []string + if searchPattern != "" { + args = append(args, "--search-for", searchPattern) + } + if name != "" { + args = append(args, "--group", name) + } + if length > 0 { + args = append(args, fmt.Sprintf("--length %d", length)) + } + if prefixLength > 0 { + args = append(args, fmt.Sprintf("--prefix-length %d", prefixLength)) + } + + var err error + args, err = shlex.Split(strings.Join(args, " "), true) + if err != nil { + return fmt.Errorf("failed to parse arguments: %w", err) + } + cmd.Args = append(cmd.Args, args...) + + fmt.Printf("running: %s\n", strings.Join(cmd.Args, " ")) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to start command: %w", err) + } + + if err := cmd.Wait(); err != nil { + return fmt.Errorf("command execution failed: %w", err) + } + + return nil +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/download.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/download.go new file mode 100644 index 000000000..0230a7253 --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/download.go @@ -0,0 +1,78 @@ +package commands + +import ( + "fmt" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config" + "github.com/spf13/cobra" +) + +func Download(appConfig config.Application) *cobra.Command { + var configs []config.BinaryFromImage + + var skipSnippets bool + + cmd := &cobra.Command{ + Use: "download", + Short: "download binaries [name@version ...]", + PreRunE: func(cmd *cobra.Command, args []string) error { + + if len(args) > 0 { + for _, arg := range args { + binaryFromImageCfg := appConfig.GetBinaryFromImage(arg, "") + if binaryFromImageCfg == nil { + return fmt.Errorf("no config found for %q", arg) + } + configs = append(configs, *binaryFromImageCfg) + } + } else { + configs = appConfig.FromImages + } + + if skipSnippets { + var err error + configs, err = configsWithoutSnippets(appConfig, configs) + if err != nil { + return err + } + } + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + for _, binaryFromImageCfg := range configs { + if err := internal.DownloadFromImage(appConfig.DownloadPath, binaryFromImageCfg); err != nil { + return err + } + } + + if len(configs) == 0 { + fmt.Println("no binaries to download") + } + + return nil + }, + } + + cmd.Flags().BoolVarP(&skipSnippets, "skip-if-covered-by-snippet", "s", false, "skip downloading entries already covered by snippets") + + return cmd +} + +func configsWithoutSnippets(appConfig config.Application, configs []config.BinaryFromImage) ([]config.BinaryFromImage, error) { + entries, err := internal.ListAllEntries(appConfig) + if err != nil { + return nil, err + } + + var filtered []config.BinaryFromImage + + for _, cfg := range configs { + if entries.BinaryFromImageHasSnippet(cfg) { + continue + } + filtered = append(filtered, cfg) + } + + return filtered, nil +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/list.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/list.go new file mode 100644 index 000000000..82b127167 --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/list.go @@ -0,0 +1,91 @@ +package commands + +import ( + "fmt" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config" + "github.com/jedib0t/go-pretty/v6/table" + "github.com/spf13/cobra" + "strings" +) + +func List(appConfig config.Application) *cobra.Command { + + var showPaths bool + + cmd := &cobra.Command{ + Use: "list", + Short: "list managed binaries and managed/unmanaged snippets", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + + return runList(appConfig, showPaths) + }, + } + + cmd.Flags().BoolVarP(&showPaths, "show-paths", "p", false, "show paths to binaries and snippets") + + return cmd +} + +func runList(appConfig config.Application, showPaths bool) error { + + material, err := internal.ListAllEntries(appConfig) + if err != nil { + return err + } + + report := renderCatalogerListTable(material, showPaths) + + fmt.Println(report) + + return nil +} + +func renderCatalogerListTable(material map[internal.LogicalEntryKey]internal.EntryInfo, showPaths bool) string { + t := table.NewWriter() + t.SetStyle(table.StyleLight) + t.AppendHeader(table.Row{"Group", "Version", "Platform", "Name", "Configured?", "Binary", "Snippet"}) + + keys := internal.NewLogicalEntryKeys(material) + + for _, k := range keys { + info := material[k] + + isConfigured := "" + if info.IsConfigured { + isConfigured = "yes" + } + + bin := "" + snippet := "" + if showPaths { + bin = info.BinaryPath + snippet = info.SnippetPath + } else { + if info.BinaryPath != "" { + bin = "yes" + } + + if info.SnippetPath != "" { + snippet = "yes" + } + } + + t.AppendRow(table.Row{ + k.OrgName, + k.Version, + displayPlatform(k.Platform), + k.Filename, + isConfigured, + bin, + snippet, + }) + } + + return t.Render() +} + +func displayPlatform(platform string) string { + return strings.ReplaceAll(platform, "-", "/") +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/root.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/root.go new file mode 100644 index 000000000..b240868e9 --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/root.go @@ -0,0 +1,13 @@ +package commands + +import ( + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config" + "github.com/spf13/cobra" +) + +func Root(_ config.Application) *cobra.Command { + return &cobra.Command{ + Use: "manager", + Short: "manager is a tool for managing binaries and snippets", + } +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/write_snippet.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/write_snippet.go new file mode 100644 index 000000000..a0880455c --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli/commands/write_snippet.go @@ -0,0 +1,256 @@ +package commands + +import ( + "debug/elf" + "debug/macho" + "debug/pe" + "fmt" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config" + "github.com/spf13/cobra" + "gopkg.in/yaml.v3" + "io" + "os" + "path/filepath" + "strings" +) + +func WriteSnippet(appConfig config.Application) *cobra.Command { + var offset, length int + var name, version string + var binaryPath string + + cmd := &cobra.Command{ + Use: "write-snippet [binary]", + Short: "capture snippets from binaries", + Args: cobra.ExactArgs(1), + PreRunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 && (name != "" || version != "") { + return fmt.Errorf("cannot provide name or version without a binary path") + } + + binaryPath = args[0] + if _, err := os.Stat(binaryPath); err != nil { + return fmt.Errorf("unable to stat %q: %w", binaryPath, err) + } + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + platform, err := getPlatform(binaryPath) + if err != nil { + return fmt.Errorf("unable to get platform: %w", err) + } + + snippetPath, err := getSnippetPath(appConfig, binaryPath, name, version, platform) + if err != nil { + return fmt.Errorf("unable to get snippet path: %w", err) + } + + return runWriteSnippet(binaryPath, offset, length, snippetPath) + }, + } + + cmd.Flags().IntVar(&offset, "offset", -1, "the offset in the binary to start the snippet") + cmd.Flags().IntVar(&length, "length", 100, "the length of the snippet to capture") + cmd.Flags().StringVar(&name, "name", "", "the name of the snippet") + cmd.Flags().StringVar(&version, "version", "", "the version of the snippet") + + return cmd +} + +func runWriteSnippet(binaryPath string, offset, length int, snippetPath string) error { + f, err := os.Open(binaryPath) + if err != nil { + return fmt.Errorf("unable to open binary %q: %w", binaryPath, err) + } + + n, err := f.Seek(int64(offset), io.SeekStart) + if err != nil { + return fmt.Errorf("unable to seek to offset %d: %w", offset, err) + } + + if n != int64(offset) { + return fmt.Errorf("unexpectd to seek value: %d != %d", offset, n) + } + + buf := make([]byte, length) + n2, err := f.Read(buf) + if err != nil { + return fmt.Errorf("unable to read %d bytes: %w", length, err) + } + + if n2 != length { + return fmt.Errorf("unexpected read length: %d != %d", length, n2) + } + + fileDigest, err := internal.Sha256SumFile(f) + if err != nil { + return err + } + + metadata := internal.SnippetMetadata{ + Name: filepath.Base(binaryPath), + Offset: offset, + Length: length, + SnippetSha256: internal.Sha256SumBytes(buf), + FileSha256: fileDigest, + } + + metadataBytes, err := yaml.Marshal(metadata) + if err != nil { + return fmt.Errorf("unable to marshal metadata: %w", err) + } + + splitter := []byte(fmt.Sprintf("\n### byte snippet to follow ###\n")) + + var finalBuf []byte + finalBuf = append(finalBuf, metadataBytes...) + finalBuf = append(finalBuf, splitter...) + finalBuf = append(finalBuf, buf...) + + if err := os.MkdirAll(filepath.Dir(snippetPath), 0755); err != nil { + return fmt.Errorf("unable to create destination directory: %w", err) + } + + if err := os.WriteFile(snippetPath, finalBuf, 0644); err != nil { + return fmt.Errorf("unable to write snippet: %w", err) + } + + fmt.Printf("wrote snippet to %q\n", snippetPath) + + return nil +} + +func getSnippetPath(appConfig config.Application, binaryPath string, name, version, platform string) (string, error) { + binFilename := filepath.Base(binaryPath) + platform = config.PlatformAsValue(platform) + + // if all values provided, use them + if name != "" && version != "" && platform != "" { + return filepath.Join(appConfig.SnippetPath, name, version, platform, binFilename), nil + } + + // otherwise, try to infer them from the existing binary path + name, version, platform, err := inferInfoFromBinaryPath(appConfig, binaryPath) + if err != nil { + return "", err + } + + return filepath.Join(appConfig.SnippetPath, name, version, platform, binFilename), nil +} + +func inferInfoFromBinaryPath(appConfig config.Application, binaryPath string) (string, string, string, error) { + relativePath, err := filepath.Rel(appConfig.DownloadPath, binaryPath) + if err != nil { + return "", "", "", fmt.Errorf("unable to get relative path: %w", err) + } + + // otherwise, try to infer them from the existing binary path + items := internal.SplitFilepath(relativePath) + if len(items) != 4 { + return "", "", "", fmt.Errorf("too few fields: %q", binaryPath) + } + + name := items[0] + version := items[1] + platform := items[2] + + return name, version, platform, nil +} + +// getPlatform will return - for the given binary path, where os can be "linux", "darwin", "windows", +// and arch can be "amd64", "arm64", "arm", etc. +func getPlatform(binaryPath string) (string, error) { + f, err := os.Open(binaryPath) + if err != nil { + return "", fmt.Errorf("unable to open binary %q: %w", binaryPath, err) + } + + elfPlatform := getPlatformElf(f) + if elfPlatform != "" { + return elfPlatform, nil + } + + macPlatform := getPlatformMac(f) + if macPlatform != "" { + return macPlatform, nil + } + + winPlatform := getPlatformWindows(f) + if winPlatform != "" { + return winPlatform, nil + } + + // attempt to infer from the path. It is possible to see invalid-looking binaries that are still something + // we'd like to detect. + items := internal.SplitFilepath(binaryPath) + if len(items) > 2 { + candidate := items[len(items)-2] + if strings.Contains(candidate, "linux") || strings.Contains(candidate, "darwin") || strings.Contains(candidate, "windows") { + return candidate, nil + } + } + + return "", fmt.Errorf("unable to determine platform for %q", binaryPath) +} + +func getPlatformElf(f *os.File) string { + elfFile, err := elf.NewFile(f) + if err != nil { + return "" + } + + var arch string + switch elfFile.Machine { + case elf.EM_X86_64: + arch = "amd64" + case elf.EM_AARCH64: + arch = "arm64" + // TODO... + default: + arch = fmt.Sprintf("unknown-%x", elfFile.Machine) + } + + return fmt.Sprintf("linux-%s", arch) +} + +func getPlatformMac(f *os.File) string { + machoFile, err := macho.NewFile(f) + if err != nil { + return "" + } + + var arch string + switch machoFile.Cpu { + case macho.CpuAmd64: + arch = "amd64" + case macho.CpuArm64: + arch = "arm64" + // TODO... + default: + arch = fmt.Sprintf("unknown-%x", machoFile.Cpu) + } + + return fmt.Sprintf("darwin-%s", arch) +} + +func getPlatformWindows(f *os.File) string { + peFile, err := pe.NewFile(f) + if err != nil { + return "" + } + + var arch string + switch peFile.Machine { + case pe.IMAGE_FILE_MACHINE_AMD64: + arch = "amd64" + case pe.IMAGE_FILE_MACHINE_ARM64: + arch = "arm64" + // TODO... + default: + arch = fmt.Sprintf("unknown-%x", peFile.Machine) + } + + return fmt.Sprintf("windows-%s", arch) +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config/application.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config/application.go new file mode 100644 index 000000000..54473351a --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config/application.go @@ -0,0 +1,139 @@ +package config + +import ( + "fmt" + "github.com/hashicorp/go-multierror" + "github.com/scylladb/go-set/strset" + "gopkg.in/yaml.v3" + "os" + "path/filepath" + "strings" +) + +const Path = "config.yaml" + +type Application struct { + DownloadPath string `yaml:"download-path"` + SnippetPath string `yaml:"snippet-path"` + FromImages []BinaryFromImage `yaml:"from-images"` +} + +func DefaultApplication() Application { + return Application{ + DownloadPath: "bin", + } +} + +func Read() (*Application, error) { + return read(Path) +} + +func read(path string) (*Application, error) { + appConfig := DefaultApplication() + + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + err = yaml.Unmarshal(data, &appConfig) + if err != nil { + return nil, err + } + + if err := appConfig.Validate(); err != nil { + return nil, err + } + return &appConfig, nil +} + +func (c Image) Key() string { + return fmt.Sprintf("%s:%s", c.Reference, c.Platform) +} + +func (c Application) Validate() error { + set := strset.New() + var err error + for i, entry := range c.FromImages { + key := entry.Key() + + if set.Has(key) { + err = multierror.Append(err, fmt.Errorf("duplicate entry %q", entry)) + continue + } + + set.Add(entry.Key()) + + if len(entry.PathsInImage) > 1 && entry.GenericName == "" { + err = multierror.Append(err, fmt.Errorf("specified multiple paths but no name for entry %d (%s)", i+1, key)) + } + if entry.Name() == "" { + err = multierror.Append(err, fmt.Errorf("missing name for entry %d", i+1)) + } + if entry.Version == "" { + err = multierror.Append(err, fmt.Errorf("missing version for entry %d", i+1)) + } + if len(entry.Images) == 0 { + err = multierror.Append(err, fmt.Errorf("missing images for entry %d (%s)", i+1, key)) + } + + var imageSet = strset.New() + for j, image := range entry.Images { + imgKey := image.Key() + if imageSet.Has(imgKey) { + err = multierror.Append(err, fmt.Errorf("duplicate image %q for entry %d (%s)", image.Key(), i+1, key)) + continue + } + imageSet.Add(imgKey) + + if image.Reference == "" { + err = multierror.Append(err, fmt.Errorf("missing ref reference for entry %d (%s) image %d", i+1, key, j+1)) + } + if image.Platform == "" { + err = multierror.Append(err, fmt.Errorf("missing platform for entry %d (%s) image %d", i+1, key, j+1)) + } + } + if len(entry.PathsInImage) == 0 { + err = multierror.Append(err, fmt.Errorf("missing paths for entry %d (%s)", i+1, key)) + } + + } + return err +} + +func (c Application) GetBinaryFromImage(name, version string) *BinaryFromImage { + if strings.Contains(name, "@") && version == "" { + parts := strings.Split(name, "@") + name = parts[0] + version = parts[1] + } + for _, entry := range c.FromImages { + if entry.Name() == name && entry.Version == version { + return &entry + } + } + return nil +} + +func (c Application) GetBinaryFromImageByPath(storePath string) *BinaryFromImage { + // each key is the store path except for the root (e.g. bin or snippet) + entryByStorePath := make(map[string]BinaryFromImage) + + for _, entry := range c.FromImages { + for _, path := range entry.AllStorePaths(c.DownloadPath) { + pathWithoutRoot := splitFilepath(path)[1:] + entryByStorePath[filepath.Join(pathWithoutRoot...)] = entry + } + } + + pathWithoutRoot := filepath.Join(splitFilepath(storePath)[1:]...) + if entry, ok := entryByStorePath[pathWithoutRoot]; ok { + return &entry + } + + return nil +} + +func splitFilepath(path string) []string { + return strings.Split(path, string(filepath.Separator)) +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config/binary_from_image.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config/binary_from_image.go new file mode 100644 index 000000000..ea873707d --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config/binary_from_image.go @@ -0,0 +1,79 @@ +package config + +import ( + "crypto/sha256" + "fmt" + "gopkg.in/yaml.v3" + "path/filepath" + "strings" +) + +type BinaryFromImage struct { + GenericName string `yaml:"name"` + Version string `yaml:"version"` + + Images []Image `yaml:"images"` + PathsInImage []string `yaml:"paths"` +} + +type Image struct { + Reference string `yaml:"ref"` + Platform string `yaml:"platform"` +} + +func (c BinaryFromImage) Key() string { + return fmt.Sprintf("%s:%s", c.Name(), c.Version) +} + +func (c BinaryFromImage) Name() string { + displayName := c.GenericName + if displayName == "" { + var path string + if len(c.PathsInImage) > 0 { + path = c.PathsInImage[0] + } + if path == "" { + return "" + } + return filepath.Base(path) + } + return displayName +} + +func (c BinaryFromImage) AllStorePaths(dest string) []string { + var paths []string + for _, image := range c.Images { + paths = append(paths, c.AllStorePathsForImage(image, dest)...) + } + return paths +} + +func (c BinaryFromImage) AllStorePathsForImage(image Image, dest string) []string { + var paths []string + + platform := PlatformAsValue(image.Platform) + for _, path := range c.PathsInImage { + base := filepath.Base(path) + if path == "" { + base = "" + } + paths = append(paths, filepath.Join(dest, c.Name(), c.Version, platform, base)) + } + + return paths +} + +func PlatformAsValue(platform string) string { + return strings.ReplaceAll(platform, "/", "-") +} + +func (c BinaryFromImage) Fingerprint() string { + by, err := yaml.Marshal(c) + if err != nil { + panic(err) + } + + hasher := sha256.New() + hasher.Write(by) + return fmt.Sprintf("%x", hasher.Sum(nil)) +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/download_from_image.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/download_from_image.go new file mode 100644 index 000000000..e64880be6 --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/download_from_image.go @@ -0,0 +1,157 @@ +package internal + +import ( + "fmt" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui" + "github.com/google/uuid" + "os" + "os/exec" + "path/filepath" +) + +func DownloadFromImage(dest string, config config.BinaryFromImage) error { + t := ui.Title{Name: config.Name(), Version: config.Version} + t.Start() + + hostPaths := config.AllStorePaths(dest) + if allPathsExist(hostPaths) { + if !isDownloadStale(config, hostPaths) { + t.Skip("already exists") + return nil + } else { + t.Update("stale, updating...") + } + } + + if err := pullDockerImages(config.Images); err != nil { + return err + } + + if err := copyBinariesFromDockerImages(config, dest); err != nil { + return fmt.Errorf("failed to copy binary for %s@%s: %v", config.Name(), config.Version, err) + } + + return nil +} + +func isDownloadStale(config config.BinaryFromImage, binaryPaths []string) bool { + currentFingerprint := config.Fingerprint() + + for _, path := range binaryPaths { + fingerprintPath := path + ".fingerprint" + if _, err := os.Stat(fingerprintPath); err != nil { + // missing a fingerprint file means the download is stale + return true + } + + writtenFingerprint, err := os.ReadFile(fingerprintPath) + if err != nil { + // missing a fingerprint file means the download is stale + return true + } + + if string(writtenFingerprint) != currentFingerprint { + // the fingerprint file does not match the current fingerprint, so the download is stale + return true + } + } + + return false +} + +func allPathsExist(paths []string) bool { + for _, path := range paths { + if _, err := os.Stat(path); err != nil { + return false + } + } + return true +} + +func pullDockerImages(images []config.Image) error { + for _, image := range images { + if err := pullDockerImage(image.Reference, image.Platform); err != nil { + return fmt.Errorf("failed to pull image %s for platform %s: %v", image.Reference, image.Platform, err) + } + } + return nil +} + +func pullDockerImage(imageReference, platform string) error { + a := ui.Action{Msg: fmt.Sprintf("pull image %s (%s)", imageReference, platform)} + a.Start() + cmd := exec.Command("docker", "image", "inspect", imageReference) + if err := cmd.Run(); err == nil { + a.Skip(fmt.Sprintf("docker image already exists %q", imageReference)) + + return nil + } + + cmd = exec.Command("docker", "pull", "--platform", platform, imageReference) + err := cmd.Run() + + a.Done(err) + + return err +} + +func copyBinariesFromDockerImages(config config.BinaryFromImage, destination string) (err error) { + for _, image := range config.Images { + if err := copyBinariesFromDockerImage(config, destination, image); err != nil { + return err + } + } + + return nil +} + +func copyBinariesFromDockerImage(config config.BinaryFromImage, destination string, image config.Image) (err error) { + containerName := fmt.Sprintf("%s-%s-%s", config.Name(), config.Version, uuid.New().String()) + + cmd := exec.Command("docker", "create", "--name", containerName, image.Reference) + if err = cmd.Run(); err != nil { + return err + } + + defer func() { + cmd := exec.Command("docker", "rm", containerName) + cmd.Run() + }() + + for i, destinationPath := range config.AllStorePathsForImage(image, destination) { + path := config.PathsInImage[i] + if err := copyBinaryFromContainer(containerName, path, destinationPath, config.Fingerprint()); err != nil { + return err + } + } + + return nil +} + +func copyBinaryFromContainer(containerName, containerPath, destinationPath, fingerprint string) (err error) { + + a := ui.Action{Msg: fmt.Sprintf("extract %s", containerPath)} + a.Start() + + defer func() { + a.Done(err) + }() + + if err := os.MkdirAll(filepath.Dir(destinationPath), 0755); err != nil { + return err + } + + cmd := exec.Command("docker", "cp", fmt.Sprintf("%s:%s", containerName, containerPath), destinationPath) + if err := cmd.Run(); err != nil { + return err + } + + // capture fingerprint file + fingerprintPath := destinationPath + ".fingerprint" + if err := os.WriteFile(fingerprintPath, []byte(fingerprint), 0644); err != nil { + return fmt.Errorf("unable to write fingerprint file: %w", err) + } + + return nil +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/list_entries.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/list_entries.go new file mode 100644 index 000000000..f07fc5dc0 --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/list_entries.go @@ -0,0 +1,197 @@ +package internal + +import ( + "fmt" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config" + "os" + "path/filepath" + "sort" + "strings" +) + +type Entries map[LogicalEntryKey]EntryInfo + +type EntryInfo struct { + IsConfigured bool + BinaryPath string + SnippetPath string +} + +type LogicalEntryKey struct { + OrgName string + Version string + Platform string + Filename string +} + +func (k LogicalEntryKey) Path() string { + return fmt.Sprintf("%s/%s/%s/%s", k.OrgName, k.Version, k.Platform, k.Filename) +} + +type LogicalEntryKeys []LogicalEntryKey + +func (l LogicalEntryKeys) Len() int { + return len(l) +} + +func (l LogicalEntryKeys) Less(i, j int) bool { + if l[i].OrgName == l[j].OrgName { + if l[i].Version == l[j].Version { + if l[i].Platform == l[j].Platform { + return l[i].Filename < l[j].Filename + } + return l[i].Platform < l[j].Platform + } + return l[i].Version < l[j].Version + } + return l[i].OrgName < l[j].OrgName +} + +func (l LogicalEntryKeys) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + +func NewLogicalEntryKeys(m map[LogicalEntryKey]EntryInfo) LogicalEntryKeys { + var keys LogicalEntryKeys + for k := range m { + keys = append(keys, k) + } + sort.Sort(keys) + return keys +} + +func ListAllBinaries(appConfig config.Application) (Entries, error) { + binaries, err := allFilePaths(appConfig.DownloadPath) + if err != nil { + return nil, fmt.Errorf("unable to list binaries: %w", err) + } + + cases := make(map[LogicalEntryKey]EntryInfo) + for _, storePath := range binaries { + isConfigured := appConfig.GetBinaryFromImageByPath(storePath) != nil + + relativePath, err := filepath.Rel(appConfig.DownloadPath, storePath) + if err != nil { + return nil, fmt.Errorf("unable to get relative path for %q: %w", storePath, err) + } + + key, err := getLogicalKey(relativePath) + if err != nil { + return nil, fmt.Errorf("unable to get logical key for binary %q: %w", storePath, err) + } + cases[*key] = EntryInfo{ + IsConfigured: isConfigured, + BinaryPath: storePath, + } + } + + return cases, nil +} + +func ListAllEntries(appConfig config.Application) (Entries, error) { + + snippets, err := allFilePaths(appConfig.SnippetPath) + if err != nil { + return nil, fmt.Errorf("unable to list snippets: %w", err) + } + + cases, err := ListAllBinaries(appConfig) + if err != nil { + return nil, fmt.Errorf("unable to list binaries: %w", err) + } + + // anything configured that isn't in the binaries list? + for _, cfg := range appConfig.FromImages { + for _, image := range cfg.Images { + for _, path := range cfg.AllStorePathsForImage(image, appConfig.DownloadPath) { + key := newLogicalEntryForImage(cfg, image, path) + if _, ok := cases[key]; ok { + continue + } + cases[key] = EntryInfo{ + IsConfigured: true, + } + } + } + } + + // correlate snippets to existing binaries and configurations (and add unmanaged ones) + for _, storePath := range snippets { + relativePath, err := filepath.Rel(appConfig.SnippetPath, storePath) + if err != nil { + return nil, fmt.Errorf("unable to get relative path for %q: %w", storePath, err) + } + key, err := getLogicalKey(relativePath) + if err != nil { + return nil, fmt.Errorf("unable to get logical key for snippet %q: %w", storePath, err) + } + + if v, ok := cases[*key]; ok { + v.SnippetPath = storePath + cases[*key] = v + + continue + } + + cases[*key] = EntryInfo{ + IsConfigured: false, + SnippetPath: storePath, + } + } + + return cases, nil +} + +func newLogicalEntryForImage(cfg config.BinaryFromImage, image config.Image, storePath string) LogicalEntryKey { + return LogicalEntryKey{ + OrgName: cfg.Name(), + Version: cfg.Version, + Platform: config.PlatformAsValue(image.Platform), + Filename: filepath.Base(storePath), + } +} + +func getLogicalKey(managedBinaryPath string) (*LogicalEntryKey, error) { + // infer the logical key from the path alone: name/version/platform/filename + + items := SplitFilepath(managedBinaryPath) + if len(items) != 4 { + return nil, fmt.Errorf("invalid managed binary path: %q", managedBinaryPath) + } + + return &LogicalEntryKey{ + OrgName: items[0], + Version: items[1], + Platform: items[2], + Filename: items[3], + }, nil +} + +func allFilePaths(root string) ([]string, error) { + var paths []string + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if info != nil && !info.IsDir() && !strings.HasSuffix(path, ".fingerprint") { + paths = append(paths, path) + } + return nil + }) + if err != nil { + return nil, err + } + return paths, nil +} + +func (e Entries) BinaryFromImageHasSnippet(cfg config.BinaryFromImage) bool { + // all paths for all images must have snippets to return true + for _, image := range cfg.Images { + for _, storePath := range cfg.AllStorePathsForImage(image, "") { + key := newLogicalEntryForImage(cfg, image, storePath) + if v, ok := e[key]; ok { + if v.SnippetPath == "" { + return false + } + } + } + } + return true +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/snippet_metadata.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/snippet_metadata.go new file mode 100644 index 000000000..171db4a3b --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/snippet_metadata.go @@ -0,0 +1,39 @@ +package internal + +import ( + "fmt" + "gopkg.in/yaml.v3" + "os" + "strings" +) + +type SnippetMetadata struct { + Name string `yaml:"name"` + Offset int `yaml:"offset"` + Length int `yaml:"length"` + SnippetSha256 string `yaml:"snippetSha256"` + FileSha256 string `yaml:"fileSha256"` +} + +func ReadSnippetMetadata(path string) (*SnippetMetadata, error) { + if path == "" { + return nil, nil + } + + contents, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + fields := strings.Split(string(contents), "\n### byte snippet to follow ###\n") + if len(fields) != 2 { + return nil, fmt.Errorf("this is not a snippet") + } + + var metadata SnippetMetadata + if err := yaml.Unmarshal([]byte(fields[0]), &metadata); err != nil { + return nil, err + } + + return &metadata, nil +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/action.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/action.go new file mode 100644 index 000000000..00d6381e8 --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/action.go @@ -0,0 +1,51 @@ +package ui + +import ( + "errors" + "fmt" + "os/exec" + "strings" +) + +type Action struct { + Msg string +} + +func (a Action) Start() { + fmt.Printf(" • %s%s%s\n", purple+italic, a.Msg, reset) +} + +func (a Action) Skip(newMsg ...string) { + if len(newMsg) > 0 { + // clear the line + goToPreviousLineStart() + // add a little extra to account for ansi escape codes (hack) + fmt.Printf("%s\n", strings.Repeat(" ", len(a.Msg)+10)) + a.Msg = newMsg[0] + } + goToPreviousLineStart() + formatSkip(a.Msg) +} + +func (a Action) Done(err error) { + goToPreviousLineStart() + if err != nil { + + fmt.Printf(" %s✗%s %s%s%s\n", red+bold, reset, red, a.Msg, reset) + + var exitError *exec.ExitError + if errors.As(err, &exitError) && len(exitError.Stderr) > 0 { + fmt.Printf(" %s├──%s %s%s%s\n", grey, reset, red, err, reset) + fmt.Printf(" %s└──%s %s%s%s\n", grey, reset, red, "stderr:", reset) + fmt.Println(string(exitError.Stderr)) + } else { + fmt.Printf(" %s└──%s %s%s%s\n", grey, reset, red, err, reset) + } + return + } + fmt.Printf(" %s✔%s %s\n", green+bold, reset, a.Msg) +} + +func formatSkip(msg string) { + fmt.Printf(" %s⏭%s %s%s%s\n", bold, reset, grey, msg, reset) +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/ansi.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/ansi.go new file mode 100644 index 000000000..6be89faa4 --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/ansi.go @@ -0,0 +1,17 @@ +package ui + +import "fmt" + +const ( + grey = "\033[90m" + reset = "\033[0m" + bold = "\033[1m" + red = "\033[31m" + italic = "\033[3m" + purple = "\033[95m" // hi variant + green = "\033[32m" +) + +func goToPreviousLineStart() { + fmt.Printf("\033[F") +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/binary_list.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/binary_list.go new file mode 100644 index 000000000..b551ead9e --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/binary_list.go @@ -0,0 +1,107 @@ +package ui + +import ( + "fmt" + "github.com/charmbracelet/bubbles/list" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "os" +) + +var quitTextStyle = lipgloss.NewStyle().Margin(1, 0, 2, 4) + +type item string + +func (i item) Title() string { return string(i) } +func (i item) Description() string { return "" } +func (i item) FilterValue() string { return string(i) } + +type model struct { + list list.Model + choice string + quitting bool +} + +func PromptSelectBinary(binaryPaths []string) (string, error) { + var items []list.Item + for _, p := range binaryPaths { + items = append(items, item(p)) + } + + d := list.NewDefaultDelegate() + d.ShowDescription = false + d.Styles.NormalTitle = d.Styles.NormalTitle.PaddingLeft(4) + d.Styles.SelectedTitle = d.Styles.SelectedTitle.PaddingLeft(3) + d.SetSpacing(0) + + l := list.New(items, d, 80, 80) + l.Title = "Select a binary to capture a snippet from:" + l.SetShowStatusBar(false) + l.SetFilteringEnabled(true) + l.Styles.Title = lipgloss.NewStyle().Bold(true).MarginLeft(1) + l.Styles.PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) + l.Styles.HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) + + m := model{list: l} + + p := tea.NewProgram(m, tea.WithAltScreen()) + + fm, err := p.Run() + if err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } + + m = fm.(model) + + if m.quitting { + return "", fmt.Errorf("cancelled") + } + + if m.choice == "" { + return "", fmt.Errorf("no binary selected") + } + + return m.choice, nil +} + +func (m model) Init() tea.Cmd { + return nil +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.list.SetWidth(msg.Width) + m.list.SetHeight(msg.Height) + return m, nil + + case tea.KeyMsg: + switch keypress := msg.String(); keypress { + case "ctrl+c": + m.quitting = true + return m, tea.Quit + + case "enter": + i, ok := m.list.SelectedItem().(item) + if ok { + m.choice = string(i) + } + return m, tea.Quit + } + } + + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +func (m model) View() string { + if m.choice != "" { + return quitTextStyle.Render(fmt.Sprintf("Selected %q", m.choice)) + } + if m.quitting { + return quitTextStyle.Render("Cancelled") + } + return "\n" + m.list.View() +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/error.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/error.go new file mode 100644 index 000000000..c107ad34a --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/error.go @@ -0,0 +1,7 @@ +package ui + +import "fmt" + +func RenderError(err error) string { + return fmt.Sprintf("%s%v%s", red, err, reset) +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/title.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/title.go new file mode 100644 index 000000000..9da59b14a --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui/title.go @@ -0,0 +1,33 @@ +package ui + +import ( + "fmt" + "strings" +) + +type Title struct { + Name, Version string +} + +func (t Title) Start() { + t.start() + fmt.Println() +} + +func (t Title) start() { + fmt.Printf("%s%s@%s%s", bold, t.Name, t.Version, reset) +} + +func (t Title) Update(msg string) { + goToPreviousLineStart() + t.start() + fmt.Print(strings.Repeat(" ", 35-(len(t.Name)+len(t.Version)))) + fmt.Printf(" %s⚠%s %s%s%s\n", bold, reset, italic+grey, msg, reset) +} + +func (t Title) Skip(msg string) { + goToPreviousLineStart() + t.start() + fmt.Print(strings.Repeat(" ", 35-(len(t.Name)+len(t.Version)))) + formatSkip(msg) +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/internal/utils.go b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/utils.go new file mode 100644 index 000000000..ab2578f80 --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/internal/utils.go @@ -0,0 +1,34 @@ +package internal + +import ( + "crypto/sha256" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +func SplitFilepath(path string) []string { + return strings.Split(path, string(filepath.Separator)) +} + +func Sha256SumFile(f *os.File) (string, error) { + _, err := f.Seek(0, io.SeekStart) + if err != nil { + return "", fmt.Errorf("unable to seek to start of file: %w", err) + } + hasher := sha256.New() + _, err = io.Copy(hasher, f) + if err != nil { + return "", fmt.Errorf("unable to hash file: %w", err) + } + + return fmt.Sprintf("%x", hasher.Sum(nil)), nil +} + +func Sha256SumBytes(buf []byte) string { + hasher := sha256.New() + hasher.Write(buf) + return fmt.Sprintf("%x", hasher.Sum(nil)) +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/main.go b/syft/pkg/cataloger/binary/test-fixtures/manager/main.go new file mode 100644 index 000000000..d6b35fe3f --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/main.go @@ -0,0 +1,26 @@ +package main + +import ( + "fmt" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/cli" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/ui" + "os" +) + +func main() { + cmd, err := cli.New() + if err != nil { + exit(err) + } + + if err := cmd.Execute(); err != nil { + exit(err) + } +} + +func exit(err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", ui.RenderError(err)) + } + os.Exit(1) +} diff --git a/syft/pkg/cataloger/binary/test-fixtures/manager/testutil/snippet_or_binary.go b/syft/pkg/cataloger/binary/test-fixtures/manager/testutil/snippet_or_binary.go new file mode 100644 index 000000000..a95df4f6c --- /dev/null +++ b/syft/pkg/cataloger/binary/test-fixtures/manager/testutil/snippet_or_binary.go @@ -0,0 +1,100 @@ +package testutil + +import ( + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal" + "github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config" + "github.com/stretchr/testify/require" + "os" + "path/filepath" + "testing" +) + +// SnippetOrBinary returns the path to either the binary or the snippet for the given logical entry key. +// Note: this is intended to be used only within the context of the binary cataloger test fixtures. Any other +// use is unsupported. Path should be a logical path relative to the test-fixtures/classifiers directory (but does +// not specify the "bin" or "snippets" parent path... this is determined logically [snippets > binary unless told +// otherwise]). Path should also be to the directory containing the binary or snippets of interest (not the binaries +// or snippets itself). +func SnippetOrBinary(t *testing.T, path string, requireBinary bool) string { + t.Helper() + + require.Len(t, internal.SplitFilepath(path), 3, "path must be a in the form //") + + // cd to test-fixtures directory and load the config + + cwd, err := os.Getwd() + require.NoError(t, err) + + require.NoError(t, os.Chdir("test-fixtures")) + defer func() { + require.NoError(t, os.Chdir(cwd)) + }() + + appConfig, err := config.Read() + require.NoError(t, err) + + // find the first matching fixture path that matches the given requirements + + entries, err := internal.ListAllEntries(*appConfig) + require.NoError(t, err) + + var fixturePath string + for k, v := range entries { + if filepath.Dir(k.Path()) == path { + // prefer the snippet over the binary + if !requireBinary { + if v.SnippetPath != "" { + t.Logf("using snippet for %q", path) + validateSnippet(t, v.BinaryPath, v.SnippetPath) + fixturePath = v.SnippetPath + break + } + if v.BinaryPath != "" { + fixturePath = v.BinaryPath + break + } + t.Fatalf("no binary or snippet found for %q", path) + } + if v.BinaryPath != "" { + t.Logf("forcing the use of the original binary for %q", path) + fixturePath = v.BinaryPath + break + } + + t.Fatalf("no binary found for %q", path) + } + } + + if fixturePath == "" { + t.Fatalf("no fixture found for %q", path) + } + + // this should be relative to the tests-fixtures directory and should be the directory containing the binary or + // snippet of interest (not the path to the binary or snippet itself) + return filepath.Join("test-fixtures", filepath.Dir(fixturePath)) +} + +func validateSnippet(t *testing.T, binaryPath, snippetPath string) { + t.Helper() + + // get a sha256 of the binary + if _, err := os.Stat(binaryPath); err != nil { + // no binary to validate against (this is ok) + return + } + + metadata, err := internal.ReadSnippetMetadata(snippetPath) + require.NoError(t, err) + + if metadata == nil { + return + } + + f, err := os.Open(binaryPath) + require.NoError(t, err) + expected, err := internal.Sha256SumFile(f) + require.NoError(t, err) + + require.Equal(t, expected, metadata.FileSha256, "snippet shadows a binary with a different sha256") + +}