From e2ed89f7001b657e0c4b5190db03623bdafa95bf Mon Sep 17 00:00:00 2001 From: William Murphy Date: Fri, 16 Jun 2023 13:26:18 -0400 Subject: [PATCH 01/11] Pad artifact IDs (#1882) Otherwise the hash can sometimes be short if it results in a low uint64. Signed-off-by: Will Murphy --- syft/artifact/id.go | 2 +- .../test-fixtures/snapshot/TestEncodeFullJSONDocument.golden | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/syft/artifact/id.go b/syft/artifact/id.go index 4b87fd293c1..c53ca7c9ad0 100644 --- a/syft/artifact/id.go +++ b/syft/artifact/id.go @@ -22,5 +22,5 @@ func IDByHash(obj interface{}) (ID, error) { return "", fmt.Errorf("could not build ID for object=%+v: %w", obj, err) } - return ID(fmt.Sprintf("%x", f)), nil + return ID(fmt.Sprintf("%016x", f)), nil } diff --git a/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden b/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden index c039a245730..daca7eb5ed6 100644 --- a/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden +++ b/syft/formats/syftjson/test-fixtures/snapshot/TestEncodeFullJSONDocument.golden @@ -91,7 +91,7 @@ } }, { - "id": "e7c88bd18e11b0b", + "id": "0e7c88bd18e11b0b", "location": { "path": "/a/place/a" }, From 269006bf046f197d1b7a73c294b677dcdb9965e4 Mon Sep 17 00:00:00 2001 From: "anchore-actions-token-generator[bot]" <102182147+anchore-actions-token-generator[bot]@users.noreply.github.com> Date: Tue, 20 Jun 2023 10:22:18 -0400 Subject: [PATCH 02/11] chore(deps): update bootstrap tools to latest versions (#1880) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 08865e05578..eb1d67e0bc4 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ CHRONICLE_CMD = $(TEMP_DIR)/chronicle GLOW_CMD = $(TEMP_DIR)/glow # Tool versions ################################# -GOLANGCILINT_VERSION := v1.53.2 +GOLANGCILINT_VERSION := v1.53.3 GOSIMPORTS_VERSION := v0.3.8 BOUNCER_VERSION := v0.4.0 CHRONICLE_VERSION := v0.6.0 From 631d50d03883d83ee51269be1dcfb3146ede500f Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Tue, 20 Jun 2023 11:47:02 -0400 Subject: [PATCH 03/11] chore: update SPDX license list to 3.21 (#1885) --- internal/spdxlicense/license_list.go | 46 ++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/internal/spdxlicense/license_list.go b/internal/spdxlicense/license_list.go index 01513793762..29db460f8da 100644 --- a/internal/spdxlicense/license_list.go +++ b/internal/spdxlicense/license_list.go @@ -1,9 +1,9 @@ // Code generated by go generate; DO NOT EDIT. -// This file was generated by robots at 2023-03-22 18:00:44.589388 -0400 EDT m=+0.160679640 +// This file was generated by robots at 2023-06-20 11:37:07.979104 -0400 EDT m=+0.478800893 // using data from https://spdx.org/licenses/licenses.json package spdxlicense -const Version = "3.20" +const Version = "3.21" var licenseIDs = map[string]string{ "0bsd": "0BSD", @@ -93,6 +93,11 @@ var licenseIDs = map[string]string{ "artistic2": "Artistic-2.0", "artistic2.0": "Artistic-2.0", "artistic2.0.0": "Artistic-2.0", + "aswfdigitalassets1": "ASWF-Digital-Assets-1.0", + "aswfdigitalassets1.0": "ASWF-Digital-Assets-1.0", + "aswfdigitalassets1.0.0": "ASWF-Digital-Assets-1.0", + "aswfdigitalassets1.1": "ASWF-Digital-Assets-1.1", + "aswfdigitalassets1.1.0": "ASWF-Digital-Assets-1.1", "baekmuk": "Baekmuk", "bahyph": "Bahyph", "barr": "Barr", @@ -108,6 +113,7 @@ var licenseIDs = map[string]string{ "blueoak1": "BlueOak-1.0.0", "blueoak1.0": "BlueOak-1.0.0", "blueoak1.0.0": "BlueOak-1.0.0", + "boehmgc": "Boehm-GC", "borceux": "Borceux", "briangladman3.0.0clause": "Brian-Gladman-3-Clause", "briangladman3.0clause": "Brian-Gladman-3-Clause", @@ -336,10 +342,13 @@ var licenseIDs = map[string]string{ "ccbysa3.0.0": "CC-BY-SA-3.0", "ccbysa3.0.0at": "CC-BY-SA-3.0-AT", "ccbysa3.0.0de": "CC-BY-SA-3.0-DE", + "ccbysa3.0.0igo": "CC-BY-SA-3.0-IGO", "ccbysa3.0at": "CC-BY-SA-3.0-AT", "ccbysa3.0de": "CC-BY-SA-3.0-DE", + "ccbysa3.0igo": "CC-BY-SA-3.0-IGO", "ccbysa3at": "CC-BY-SA-3.0-AT", "ccbysa3de": "CC-BY-SA-3.0-DE", + "ccbysa3igo": "CC-BY-SA-3.0-IGO", "ccbysa4": "CC-BY-SA-4.0", "ccbysa4.0": "CC-BY-SA-4.0", "ccbysa4.0.0": "CC-BY-SA-4.0", @@ -440,6 +449,7 @@ var licenseIDs = map[string]string{ "drl1.0": "DRL-1.0", "drl1.0.0": "DRL-1.0", "dsdp": "DSDP", + "dtoa": "dtoa", "dvipdfm": "dvipdfm", "ecl1": "ECL-1.0", "ecl1.0": "ECL-1.0", @@ -638,6 +648,9 @@ var licenseIDs = map[string]string{ "imlib2.0": "Imlib2", "imlib2.0.0": "Imlib2", "infozip": "Info-ZIP", + "innernet2": "Inner-Net-2.0", + "innernet2.0": "Inner-Net-2.0", + "innernet2.0.0": "Inner-Net-2.0", "intel": "Intel", "intelacpi": "Intel-ACPI", "interbase1": "Interbase-1.0", @@ -663,8 +676,11 @@ var licenseIDs = map[string]string{ "lal1.3": "LAL-1.3", "lal1.3.0": "LAL-1.3", "latex2.0.0e": "Latex2e", + "latex2.0.0etranslatednotice": "Latex2e-translated-notice", "latex2.0e": "Latex2e", + "latex2.0etranslatednotice": "Latex2e-translated-notice", "latex2e": "Latex2e", + "latex2etranslatednotice": "Latex2e-translated-notice", "leptonica": "Leptonica", "lgpl2": "LGPL-2.0-only", "lgpl2+": "LGPL-2.0-or-later", @@ -717,7 +733,14 @@ var licenseIDs = map[string]string{ "liliqrplus1": "LiLiQ-Rplus-1.1", "liliqrplus1.1": "LiLiQ-Rplus-1.1", "liliqrplus1.1.0": "LiLiQ-Rplus-1.1", + "linuxmanpages1.0.0para": "Linux-man-pages-1-para", + "linuxmanpages1.0para": "Linux-man-pages-1-para", + "linuxmanpages1para": "Linux-man-pages-1-para", "linuxmanpagescopyleft": "Linux-man-pages-copyleft", + "linuxmanpagescopyleft2.0.0para": "Linux-man-pages-copyleft-2-para", + "linuxmanpagescopyleft2.0para": "Linux-man-pages-copyleft-2-para", + "linuxmanpagescopyleft2para": "Linux-man-pages-copyleft-2-para", + "linuxmanpagescopyleftvar": "Linux-man-pages-copyleft-var", "linuxopenib": "Linux-OpenIB", "loop": "LOOP", "lpl1": "LPL-1.0", @@ -746,6 +769,7 @@ var licenseIDs = map[string]string{ "lzmasdk9to9.20": "LZMA-SDK-9.11-to-9.20", "makeindex": "MakeIndex", "martinbirgmeier": "Martin-Birgmeier", + "metamail": "metamail", "minpack": "Minpack", "miros": "MirOS", "mit": "MIT", @@ -754,6 +778,7 @@ var licenseIDs = map[string]string{ "mitcmu": "MIT-CMU", "mitenna": "MIT-enna", "mitfeh": "MIT-feh", + "mitfestival": "MIT-Festival", "mitmodernvariant": "MIT-Modern-Variant", "mitnfa": "MITNFA", "mitopengroup": "MIT-open-group", @@ -810,6 +835,7 @@ var licenseIDs = map[string]string{ "nicta1.0.0": "NICTA-1.0", "nistpd": "NIST-PD", "nistpdfallback": "NIST-PD-fallback", + "nistsoftware": "NIST-Software", "nlod1": "NLOD-1.0", "nlod1.0": "NLOD-1.0", "nlod1.0.0": "NLOD-1.0", @@ -908,6 +934,9 @@ var licenseIDs = map[string]string{ "oldap2.7.0": "OLDAP-2.7", "oldap2.8": "OLDAP-2.8", "oldap2.8.0": "OLDAP-2.8", + "olfl1": "OLFL-1.3", + "olfl1.3": "OLFL-1.3", + "olfl1.3.0": "OLFL-1.3", "oml": "OML", "openpbs2": "OpenPBS-2.3", "openpbs2.3": "OpenPBS-2.3", @@ -916,6 +945,9 @@ var licenseIDs = map[string]string{ "opl1": "OPL-1.0", "opl1.0": "OPL-1.0", "opl1.0.0": "OPL-1.0", + "opluk3": "OPL-UK-3.0", + "opluk3.0": "OPL-UK-3.0", + "opluk3.0.0": "OPL-UK-3.0", "opubl1": "OPUBL-1.0", "opubl1.0": "OPUBL-1.0", "opubl1.0.0": "OPUBL-1.0", @@ -1007,6 +1039,9 @@ var licenseIDs = map[string]string{ "sgib2": "SGI-B-2.0", "sgib2.0": "SGI-B-2.0", "sgib2.0.0": "SGI-B-2.0", + "sgp4": "SGP4", + "sgp4.0": "SGP4", + "sgp4.0.0": "SGP4", "shl0.5": "SHL-0.5", "shl0.5.0": "SHL-0.5", "shl0.51": "SHL-0.51", @@ -1052,6 +1087,7 @@ var licenseIDs = map[string]string{ "taprohl1.0.0": "TAPR-OHL-1.0", "tcl": "TCL", "tcpwrappers": "TCP-wrappers", + "termreadkey": "TermReadKey", "tmate": "TMate", "torque1": "TORQUE-1.1", "torque1.1": "TORQUE-1.1", @@ -1079,6 +1115,7 @@ var licenseIDs = map[string]string{ "unicodedfs2016.0": "Unicode-DFS-2016", "unicodedfs2016.0.0": "Unicode-DFS-2016", "unicodetou": "Unicode-TOU", + "unixcrypt": "UnixCrypt", "unlicense": "Unlicense", "upl1": "UPL-1.0", "upl1.0": "UPL-1.0", @@ -1103,6 +1140,7 @@ var licenseIDs = map[string]string{ "watcom1": "Watcom-1.0", "watcom1.0": "Watcom-1.0", "watcom1.0.0": "Watcom-1.0", + "widgetworkshop": "Widget-Workshop", "wsuipa": "Wsuipa", "wtfpl": "WTFPL", "wxwindows": "wxWindows", @@ -1112,7 +1150,11 @@ var licenseIDs = map[string]string{ "x11.0.0distributemodificationsvariant": "X11-distribute-modifications-variant", "x11.0distributemodificationsvariant": "X11-distribute-modifications-variant", "x11distributemodificationsvariant": "X11-distribute-modifications-variant", + "xdebug1": "Xdebug-1.03", + "xdebug1.03": "Xdebug-1.03", + "xdebug1.03.0": "Xdebug-1.03", "xerox": "Xerox", + "xfig": "Xfig", "xfree861": "XFree86-1.1", "xfree861.1": "XFree86-1.1", "xfree861.1.0": "XFree86-1.1", From 5d54e6e847192f63db80c9a7ee23197476c632ce Mon Sep 17 00:00:00 2001 From: William Murphy Date: Tue, 20 Jun 2023 12:08:35 -0400 Subject: [PATCH 04/11] Configure chronicle to pre-1.0 mode (#1886) Track a chronicle config file that causes chronicle to bump minor version instead of major version in response to the "breaking-change" label for pre-1.0 releases. Signed-off-by: Will Murphy --- .chronicle.yaml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .chronicle.yaml diff --git a/.chronicle.yaml b/.chronicle.yaml new file mode 100644 index 00000000000..400437c89ad --- /dev/null +++ b/.chronicle.yaml @@ -0,0 +1 @@ +enforce-v0: true # don't make breaking-change label bump major version before 1.0. From c27d5b11d4beac677e95f193ce07b15fb0847b56 Mon Sep 17 00:00:00 2001 From: Tim Gerla Date: Tue, 20 Jun 2023 15:47:50 -0400 Subject: [PATCH 05/11] docs: clarify reasoning of default catalogers for images or directories (#1887) Add some explanation around why there are different default sets of catalogers for image scans versus directory scans. Hopefully clarify questions related to #1776. Signed-off-by: Timothy Gerla --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 7ec9ef00aea..1336b526f0d 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,12 @@ This default behavior can be overridden with the `default-image-pull-source` con ### Default Cataloger Configuration by scan type +Syft uses different default sets of catalogers depending on what it is scanning: a container image or a directory on disk. The default catalogers for an image scan assumes that package installation steps have already been completed. For example, Syft will identify Python packages that have egg or wheel metadata files under a site-packages directory, since this indicates software actually installed on an image. + +However, if you are scanning a directory, Syft doesn't assume that all relevant software is installed, and will use catalogers that can identify declared dependencies that may not yet be installed on the final system: for example, dependencies listed in a Python requirements.txt. + +You can override the list of enabled/disabled catalogers by using the "catalogers" keyword in the [Syft configuration file](https://github.com/anchore/syft#configuration). + ##### Image Scanning: - alpmdb - apkdb From f79cb9587f68340326e3a434c75f15ff91959b38 Mon Sep 17 00:00:00 2001 From: Keith Zantow Date: Thu, 22 Jun 2023 12:05:38 -0400 Subject: [PATCH 06/11] fix: only output valid cyclonedx license choices (#1879) * fix: only output valid cyclonedx license choices Signed-off-by: Keith Zantow * chore: update tests Signed-off-by: Keith Zantow * chore: return nil for emtpty cdx license list Signed-off-by: Keith Zantow --------- Signed-off-by: Keith Zantow --- .../common/cyclonedxhelpers/licenses.go | 44 +++++++++---------- .../common/cyclonedxhelpers/licenses_test.go | 23 +++++----- 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/syft/formats/common/cyclonedxhelpers/licenses.go b/syft/formats/common/cyclonedxhelpers/licenses.go index 731d33a8823..f611d6a3def 100644 --- a/syft/formats/common/cyclonedxhelpers/licenses.go +++ b/syft/formats/common/cyclonedxhelpers/licenses.go @@ -12,40 +12,36 @@ import ( // This should be a function that just surfaces licenses already validated in the package struct func encodeLicenses(p pkg.Package) *cyclonedx.Licenses { - spdxc, otherc, ex := separateLicenses(p) - if len(otherc) > 0 { + spdx, other, ex := separateLicenses(p) + out := spdx + out = append(out, other...) + + if len(other) > 0 || len(spdx) > 0 { // found non spdx related licenses // build individual license choices for each // complex expressions are not combined and set as NAME fields for _, e := range ex { - otherc = append(otherc, cyclonedx.LicenseChoice{ + if e == "" { + continue + } + out = append(out, cyclonedx.LicenseChoice{ License: &cyclonedx.License{ Name: e, }, }) } - otherc = append(otherc, spdxc...) - return &otherc - } - - if len(spdxc) > 0 { - for _, l := range ex { - spdxc = append(spdxc, cyclonedx.LicenseChoice{ - License: &cyclonedx.License{ - Name: l, - }, + } else if len(ex) > 0 { + // only expressions found + e := mergeSPDX(ex) + if e != "" { + out = append(out, cyclonedx.LicenseChoice{ + Expression: e, }) } - return &spdxc } - if len(ex) > 0 { - // only expressions found - var expressions cyclonedx.Licenses - expressions = append(expressions, cyclonedx.LicenseChoice{ - Expression: mergeSPDX(ex), - }) - return &expressions + if len(out) > 0 { + return &out } return nil @@ -185,20 +181,20 @@ func reduceOuter(expression string) string { for _, c := range expression { if string(c) == "(" && openCount > 0 { - fmt.Fprintf(&sb, "%c", c) + _, _ = fmt.Fprintf(&sb, "%c", c) } if string(c) == "(" { openCount++ continue } if string(c) == ")" && openCount > 1 { - fmt.Fprintf(&sb, "%c", c) + _, _ = fmt.Fprintf(&sb, "%c", c) } if string(c) == ")" { openCount-- continue } - fmt.Fprintf(&sb, "%c", c) + _, _ = fmt.Fprintf(&sb, "%c", c) } return sb.String() diff --git a/syft/formats/common/cyclonedxhelpers/licenses_test.go b/syft/formats/common/cyclonedxhelpers/licenses_test.go index 5f390335cb6..20672385099 100644 --- a/syft/formats/common/cyclonedxhelpers/licenses_test.go +++ b/syft/formats/common/cyclonedxhelpers/licenses_test.go @@ -18,9 +18,8 @@ func Test_encodeLicense(t *testing.T) { expected *cyclonedx.Licenses }{ { - name: "no licenses", - input: pkg.Package{}, - expected: nil, + name: "no licenses", + input: pkg.Package{}, }, { name: "no SPDX licenses", @@ -48,12 +47,12 @@ func Test_encodeLicense(t *testing.T) { expected: &cyclonedx.Licenses{ { License: &cyclonedx.License{ - Name: "FOOBAR", + ID: "MIT", }, }, { License: &cyclonedx.License{ - ID: "MIT", + Name: "FOOBAR", }, }, }, @@ -97,25 +96,25 @@ func Test_encodeLicense(t *testing.T) { expected: &cyclonedx.Licenses{ { License: &cyclonedx.License{ - Name: "FakeLicense", - URL: "htts://someurl.com", + ID: "MIT", + URL: "https://opensource.org/licenses/MIT", }, }, { License: &cyclonedx.License{ - Name: "MIT AND GPL-3.0-only", + ID: "MIT", + URL: "https://spdx.org/licenses/MIT.html", }, }, { License: &cyclonedx.License{ - ID: "MIT", - URL: "https://opensource.org/licenses/MIT", + Name: "FakeLicense", + URL: "htts://someurl.com", }, }, { License: &cyclonedx.License{ - ID: "MIT", - URL: "https://spdx.org/licenses/MIT.html", + Name: "MIT AND GPL-3.0-only", }, }, }, From 7de7a7990a0d7315bac2d02f5eb6f7989fd6ef2f Mon Sep 17 00:00:00 2001 From: Dan Luhring Date: Thu, 22 Jun 2023 14:42:10 -0400 Subject: [PATCH 07/11] fix: improve version detection in Java archive name parsing (#1889) Signed-off-by: Dan Luhring --- syft/pkg/cataloger/java/archive_filename.go | 4 ++-- syft/pkg/cataloger/java/archive_filename_test.go | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/syft/pkg/cataloger/java/archive_filename.go b/syft/pkg/cataloger/java/archive_filename.go index ba8f3b383c5..084371499d9 100644 --- a/syft/pkg/cataloger/java/archive_filename.go +++ b/syft/pkg/cataloger/java/archive_filename.go @@ -47,8 +47,8 @@ import ( // my-http2-server-5 --> name="my-http2-server", version="5" // jetpack-build235-rc5 --> name="jetpack", version="build2.0-rc5" // ironman-r4-2009 --> name="ironman", version="r4-2009" -var nameAndVersionPattern = regexp.MustCompile(`(?Ui)^(?P(?:[[:alpha:]][[:word:].]*(?:\.[[:alpha:]][[:word:].]*)*-?)+)(?:-(?P(\d.*|(build\d*.*)|(rc?\d+(?:^[[:alpha:]].*)?))))?$`) -var secondaryVersionPattern = regexp.MustCompile(`(?:[._-](?P(\d.*|(build\d*.*)|(rc?\d+(?:^[[:alpha:]].*)?))))?$`) +var nameAndVersionPattern = regexp.MustCompile(`(?Ui)^(?P(?:[[:alpha:]][[:word:].]*(?:\.[[:alpha:]][[:word:].]*)*-?)+)(?:-(?P(\d.*|(build\d+.*)|(rc?\d+(?:^[[:alpha:]].*)?))))?$`) +var secondaryVersionPattern = regexp.MustCompile(`(?:[._-](?P(\d.*|(build\d+.*)|(rc?\d+(?:^[[:alpha:]].*)?))))?$`) type archiveFilename struct { raw string diff --git a/syft/pkg/cataloger/java/archive_filename_test.go b/syft/pkg/cataloger/java/archive_filename_test.go index 110d94211ce..5752e7d1ad3 100644 --- a/syft/pkg/cataloger/java/archive_filename_test.go +++ b/syft/pkg/cataloger/java/archive_filename_test.go @@ -173,6 +173,13 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) { name: "jboss-saaj-api_1.4_spec", ty: pkg.JavaPkg, }, + { + filename: "/usr/share/java/gradle/lib/gradle-build-cache-8.1.1.jar", + version: "8.1.1", + extension: "jar", + name: "gradle-build-cache", + ty: pkg.JavaPkg, + }, } for _, test := range tests { From 25ce245c035c4f80598e2baa70d0dd1db99e5c1d Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 23 Jun 2023 11:21:22 -0400 Subject: [PATCH 08/11] Simplify the SBOM writer interface (#1892) * remove sbom.writer bytes call and consolidate helpers to options pkg Signed-off-by: Alex Goodman * dont close stdout Signed-off-by: Alex Goodman * remove close operation from multiwriter Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- cmd/syft/cli/attest/attest.go | 228 +++++++++++++--------------- cmd/syft/cli/convert/convert.go | 8 +- cmd/syft/cli/options/writer.go | 142 ++++++++++++++++- cmd/syft/cli/options/writer_test.go | 201 +++++++++++++++++++++++- cmd/syft/cli/packages/packages.go | 9 +- cmd/syft/cli/poweruser/poweruser.go | 11 +- syft/lib.go | 6 +- syft/sbom/multi_writer.go | 123 --------------- syft/sbom/multi_writer_test.go | 204 ------------------------- syft/sbom/stream_writer.go | 36 ----- syft/sbom/writer.go | 12 +- 11 files changed, 443 insertions(+), 537 deletions(-) delete mode 100644 syft/sbom/multi_writer.go delete mode 100644 syft/sbom/multi_writer_test.go delete mode 100644 syft/sbom/stream_writer.go diff --git a/cmd/syft/cli/attest/attest.go b/cmd/syft/cli/attest/attest.go index 997f3307de2..9264c8e8b95 100644 --- a/cmd/syft/cli/attest/attest.go +++ b/cmd/syft/cli/attest/attest.go @@ -17,6 +17,7 @@ import ( "github.com/anchore/syft/cmd/syft/cli/packages" "github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/config" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/ui" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/event" @@ -33,17 +34,6 @@ func Run(_ context.Context, app *config.Application, args []string) error { return err } - writer, err := options.MakeWriter(app.Outputs, app.File, app.OutputTemplatePath) - if err != nil { - return fmt.Errorf("unable to write to report destination: %w", err) - } - - defer func() { - if err := writer.Close(); err != nil { - fmt.Printf("unable to close report destination: %+v", err) - } - }() - // could be an image or a directory, with or without a scheme // TODO: validate that source is image userInput := args[0] @@ -62,7 +52,7 @@ func Run(_ context.Context, app *config.Application, args []string) error { subscription := eventBus.Subscribe() return eventloop.EventLoop( - execWorker(app, *si, writer), + execWorker(app, *si), eventloop.SetupSignals(), subscription, stereoscope.Cleanup, @@ -70,7 +60,7 @@ func Run(_ context.Context, app *config.Application, args []string) error { ) } -func buildSBOM(app *config.Application, si source.Input, writer sbom.Writer, errs chan error) ([]byte, error) { +func buildSBOM(app *config.Application, si source.Input, errs chan error) (*sbom.SBOM, error) { src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions) if cleanup != nil { defer cleanup() @@ -88,135 +78,90 @@ func buildSBOM(app *config.Application, si source.Input, writer sbom.Writer, err return nil, fmt.Errorf("no SBOM produced for %q", si.UserInput) } - // note: only works for single format no multi writer support - sBytes, err := writer.Bytes(*s) - if err != nil { - return nil, fmt.Errorf("unable to build SBOM bytes: %w", err) - } - - return sBytes, nil + return s, nil } //nolint:funlen -func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error { +func execWorker(app *config.Application, si source.Input) <-chan error { errs := make(chan error) go func() { defer close(errs) - sBytes, err := buildSBOM(app, si, writer, errs) + defer bus.Publish(partybus.Event{Type: event.Exit}) + + s, err := buildSBOM(app, si, errs) if err != nil { errs <- fmt.Errorf("unable to build SBOM: %w", err) return } - // TODO: add multi writer support - for _, o := range app.Outputs { - f, err := os.CreateTemp("", o) - if err != nil { - errs <- fmt.Errorf("unable to create temp file: %w", err) - return - } - - defer f.Close() - defer os.Remove(f.Name()) - - if _, err := f.Write(sBytes); err != nil { - errs <- fmt.Errorf("unable to write SBOM to temp file: %w", err) - return - } - - // TODO: what other validation here besides binary name? - cmd := "cosign" - if !commandExists(cmd) { - errs <- fmt.Errorf("unable to find cosign in PATH; make sure you have it installed") - return - } - - // Select Cosign predicate type based on defined output type - // As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go - var predicateType string - switch strings.ToLower(o) { - case "cyclonedx-json": - predicateType = "cyclonedx" - case "spdx-tag-value": - predicateType = "spdx" - case "spdx-json": - predicateType = "spdxjson" - default: - predicateType = "custom" - } - - args := []string{"attest", si.UserInput, "--predicate", f.Name(), "--type", predicateType} - if app.Attest.Key != "" { - args = append(args, "--key", app.Attest.Key) - } - - execCmd := exec.Command(cmd, args...) - execCmd.Env = os.Environ() - if app.Attest.Key != "" { - execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", app.Attest.Password)) - } else { - // no key provided, use cosign's keyless mode - execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1") - } - - // bus adapter for ui to hook into stdout via an os pipe - r, w, err := os.Pipe() - if err != nil { - errs <- fmt.Errorf("unable to create os pipe: %w", err) - return - } - defer w.Close() - - b := &busWriter{r: r, w: w, mon: progress.NewManual(-1)} - execCmd.Stdout = b - execCmd.Stderr = b - defer b.mon.SetCompleted() - - // attest the SBOM - err = execCmd.Run() - if err != nil { - b.mon.SetError(err) - errs <- fmt.Errorf("unable to attest SBOM: %w", err) - return - } + // note: ValidateOutputOptions ensures that there is no more than one output type + o := app.Outputs[0] + + f, err := os.CreateTemp("", o) + if err != nil { + errs <- fmt.Errorf("unable to create temp file: %w", err) + return } + defer os.Remove(f.Name()) - bus.Publish(partybus.Event{ - Type: event.Exit, - Value: func() error { return nil }, - }) - }() - return errs -} + writer, err := options.MakeSBOMWriter(app.Outputs, f.Name(), app.OutputTemplatePath) + if err != nil { + errs <- fmt.Errorf("unable to create SBOM writer: %w", err) + return + } -func ValidateOutputOptions(app *config.Application) error { - err := packages.ValidateOutputOptions(app) - if err != nil { - return err - } + if err := writer.Write(*s); err != nil { + errs <- fmt.Errorf("unable to write SBOM to temp file: %w", err) + return + } - if len(app.Outputs) > 1 { - return fmt.Errorf("multiple SBOM format is not supported for attest at this time") - } + // TODO: what other validation here besides binary name? + cmd := "cosign" + if !commandExists(cmd) { + errs <- fmt.Errorf("unable to find cosign in PATH; make sure you have it installed") + return + } - // cannot use table as default output format when using template output - if slices.Contains(app.Outputs, table.ID.String()) { - app.Outputs = []string{syftjson.ID.String()} - } + // Select Cosign predicate type based on defined output type + // As orientation, check: https://github.com/sigstore/cosign/blob/main/pkg/cosign/attestation/attestation.go + var predicateType string + switch strings.ToLower(o) { + case "cyclonedx-json": + predicateType = "cyclonedx" + case "spdx-tag-value", "spdx-tv": + predicateType = "spdx" + case "spdx-json", "json": + predicateType = "spdxjson" + default: + predicateType = "custom" + } - return nil -} + args := []string{"attest", si.UserInput, "--predicate", f.Name(), "--type", predicateType} + if app.Attest.Key != "" { + args = append(args, "--key", app.Attest.Key) + } -type busWriter struct { - w *os.File - r *os.File - hasWritten bool - mon *progress.Manual -} + execCmd := exec.Command(cmd, args...) + execCmd.Env = os.Environ() + if app.Attest.Key != "" { + execCmd.Env = append(execCmd.Env, fmt.Sprintf("COSIGN_PASSWORD=%s", app.Attest.Password)) + } else { + // no key provided, use cosign's keyless mode + execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1") + } + + log.WithFields("cmd", strings.Join(execCmd.Args, " ")).Trace("creating attestation") + + // bus adapter for ui to hook into stdout via an os pipe + r, w, err := os.Pipe() + if err != nil { + errs <- fmt.Errorf("unable to create os pipe: %w", err) + return + } + defer w.Close() + + mon := progress.NewManual(-1) -func (b *busWriter) Write(p []byte) (n int, err error) { - if !b.hasWritten { - b.hasWritten = true bus.Publish( partybus.Event{ Type: event.AttestationStarted, @@ -229,13 +174,44 @@ func (b *busWriter) Write(p []byte) (n int, err error) { Context: "cosign", }, Value: &monitor.ShellProgress{ - Reader: b.r, - Manual: b.mon, + Reader: r, + Manual: mon, }, }, ) + + execCmd.Stdout = w + execCmd.Stderr = w + + // attest the SBOM + err = execCmd.Run() + if err != nil { + mon.SetError(err) + errs <- fmt.Errorf("unable to attest SBOM: %w", err) + return + } + + mon.SetCompleted() + }() + return errs +} + +func ValidateOutputOptions(app *config.Application) error { + err := packages.ValidateOutputOptions(app) + if err != nil { + return err + } + + if len(app.Outputs) > 1 { + return fmt.Errorf("multiple SBOM format is not supported for attest at this time") } - return b.w.Write(p) + + // cannot use table as default output format when using template output + if slices.Contains(app.Outputs, table.ID.String()) { + app.Outputs = []string{syftjson.ID.String()} + } + + return nil } func commandExists(cmd string) bool { diff --git a/cmd/syft/cli/convert/convert.go b/cmd/syft/cli/convert/convert.go index 50e9d4d800b..a646bded3c7 100644 --- a/cmd/syft/cli/convert/convert.go +++ b/cmd/syft/cli/convert/convert.go @@ -14,17 +14,11 @@ import ( func Run(_ context.Context, app *config.Application, args []string) error { log.Warn("convert is an experimental feature, run `syft convert -h` for help") - writer, err := options.MakeWriter(app.Outputs, app.File, app.OutputTemplatePath) + writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath) if err != nil { return err } - defer func() { - if err := writer.Close(); err != nil { - log.Warnf("unable to write to report destination: %w", err) - } - }() - // this can only be a SBOM file userInput := args[0] diff --git a/cmd/syft/cli/options/writer.go b/cmd/syft/cli/options/writer.go index a2e0e37e953..40c8a267511 100644 --- a/cmd/syft/cli/options/writer.go +++ b/cmd/syft/cli/options/writer.go @@ -2,25 +2,37 @@ package options import ( "fmt" + "io" + "os" + "path" "strings" "github.com/hashicorp/go-multierror" + "github.com/mitchellh/go-homedir" + "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/formats" "github.com/anchore/syft/syft/formats/table" "github.com/anchore/syft/syft/formats/template" "github.com/anchore/syft/syft/sbom" ) -// makeWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer +var _ sbom.Writer = (*sbomMultiWriter)(nil) + +var _ interface { + io.Closer + sbom.Writer +} = (*sbomStreamWriter)(nil) + +// MakeSBOMWriter creates a sbom.Writer for output or returns an error. this will either return a valid writer // or an error but neither both and if there is no error, sbom.Writer.Close() should be called -func MakeWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) { - outputOptions, err := parseOutputs(outputs, defaultFile, templateFilePath) +func MakeSBOMWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Writer, error) { + outputOptions, err := parseSBOMOutputFlags(outputs, defaultFile, templateFilePath) if err != nil { return nil, err } - writer, err := sbom.NewWriter(outputOptions...) + writer, err := newSBOMMultiWriter(outputOptions...) if err != nil { return nil, err } @@ -28,8 +40,18 @@ func MakeWriter(outputs []string, defaultFile, templateFilePath string) (sbom.Wr return writer, nil } -// parseOptions utility to parse command-line option strings and retain the existing behavior of default format and file -func parseOutputs(outputs []string, defaultFile, templateFilePath string) (out []sbom.WriterOption, errs error) { +// MakeSBOMWriterForFormat creates a sbom.Writer for for the given format or returns an error. +func MakeSBOMWriterForFormat(format sbom.Format, path string) (sbom.Writer, error) { + writer, err := newSBOMMultiWriter(newSBOMWriterDescription(format, path)) + if err != nil { + return nil, err + } + + return writer, nil +} + +// parseSBOMOutputFlags utility to parse command-line option strings and retain the existing behavior of default format and file +func parseSBOMOutputFlags(outputs []string, defaultFile, templateFilePath string) (out []sbomWriterDescription, errs error) { // always should have one option -- we generally get the default of "table", but just make sure if len(outputs) == 0 { outputs = append(outputs, table.ID.String()) @@ -63,7 +85,113 @@ func parseOutputs(outputs []string, defaultFile, templateFilePath string) (out [ format = tmpl } - out = append(out, sbom.NewWriterOption(format, file)) + out = append(out, newSBOMWriterDescription(format, file)) } return out, errs } + +// sbomWriterDescription Format and path strings used to create sbom.Writer +type sbomWriterDescription struct { + Format sbom.Format + Path string +} + +func newSBOMWriterDescription(f sbom.Format, p string) sbomWriterDescription { + expandedPath, err := homedir.Expand(p) + if err != nil { + log.Warnf("could not expand given writer output path=%q: %w", p, err) + // ignore errors + expandedPath = p + } + return sbomWriterDescription{ + Format: f, + Path: expandedPath, + } +} + +// sbomMultiWriter holds a list of child sbom.Writers to apply all Write and Close operations to +type sbomMultiWriter struct { + writers []sbom.Writer +} + +type nopWriteCloser struct { + io.Writer +} + +func (n nopWriteCloser) Close() error { + return nil +} + +// newSBOMMultiWriter create all report writers from input options; if a file is not specified the given defaultWriter is used +func newSBOMMultiWriter(options ...sbomWriterDescription) (_ *sbomMultiWriter, err error) { + if len(options) == 0 { + return nil, fmt.Errorf("no output options provided") + } + + out := &sbomMultiWriter{} + + for _, option := range options { + switch len(option.Path) { + case 0: + out.writers = append(out.writers, &sbomStreamWriter{ + format: option.Format, + out: nopWriteCloser{Writer: os.Stdout}, + }) + default: + // create any missing subdirectories + dir := path.Dir(option.Path) + if dir != "" { + s, err := os.Stat(dir) + if err != nil { + err = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ? + if err != nil { + return nil, err + } + } else if !s.IsDir() { + return nil, fmt.Errorf("output path does not contain a valid directory: %s", option.Path) + } + } + fileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return nil, fmt.Errorf("unable to create report file: %w", err) + } + out.writers = append(out.writers, &sbomStreamWriter{ + format: option.Format, + out: fileOut, + }) + } + } + + return out, nil +} + +// Write writes the SBOM to all writers +func (m *sbomMultiWriter) Write(s sbom.SBOM) (errs error) { + for _, w := range m.writers { + err := w.Write(s) + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("unable to write SBOM: %w", err)) + } + } + return errs +} + +// sbomStreamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup +type sbomStreamWriter struct { + format sbom.Format + out io.Writer +} + +// Write the provided SBOM to the data stream +func (w *sbomStreamWriter) Write(s sbom.SBOM) error { + defer w.Close() + return w.format.Encode(w.out, s) +} + +// Close any resources, such as open files +func (w *sbomStreamWriter) Close() error { + if closer, ok := w.out.(io.Closer); ok { + return closer.Close() + } + return nil +} diff --git a/cmd/syft/cli/options/writer_test.go b/cmd/syft/cli/options/writer_test.go index 25a01fde952..2e251234e0f 100644 --- a/cmd/syft/cli/options/writer_test.go +++ b/cmd/syft/cli/options/writer_test.go @@ -1,12 +1,18 @@ package options import ( + "io" + "path/filepath" + "strings" "testing" + "github.com/docker/docker/pkg/homedir" "github.com/stretchr/testify/assert" + + "github.com/anchore/syft/syft/sbom" ) -func TestIsSupportedFormat(t *testing.T) { +func Test_MakeSBOMWriter(t *testing.T) { tests := []struct { outputs []string wantErr assert.ErrorAssertionFunc @@ -28,7 +34,198 @@ func TestIsSupportedFormat(t *testing.T) { } for _, tt := range tests { - _, err := MakeWriter(tt.outputs, "", "") + _, err := MakeSBOMWriter(tt.outputs, "", "") tt.wantErr(t, err) } } + +func dummyEncoder(io.Writer, sbom.SBOM) error { + return nil +} + +func dummyFormat(name string) sbom.Format { + return sbom.NewFormat(sbom.AnyVersion, dummyEncoder, nil, nil, sbom.FormatID(name)) +} + +func Test_newSBOMMultiWriter(t *testing.T) { + type writerConfig struct { + format string + file string + } + + tmp := t.TempDir() + + testName := func(options []sbomWriterDescription, err bool) string { + var out []string + for _, opt := range options { + out = append(out, string(opt.Format.ID())+"="+opt.Path) + } + errs := "" + if err { + errs = "(err)" + } + return strings.Join(out, ", ") + errs + } + + tests := []struct { + outputs []sbomWriterDescription + err bool + expected []writerConfig + }{ + { + outputs: []sbomWriterDescription{}, + err: true, + }, + { + outputs: []sbomWriterDescription{ + { + Format: dummyFormat("table"), + Path: "", + }, + }, + expected: []writerConfig{ + { + format: "table", + }, + }, + }, + { + outputs: []sbomWriterDescription{ + { + Format: dummyFormat("json"), + }, + }, + expected: []writerConfig{ + { + format: "json", + }, + }, + }, + { + outputs: []sbomWriterDescription{ + { + Format: dummyFormat("json"), + Path: "test-2.json", + }, + }, + expected: []writerConfig{ + { + format: "json", + file: "test-2.json", + }, + }, + }, + { + outputs: []sbomWriterDescription{ + { + Format: dummyFormat("json"), + Path: "test-3/1.json", + }, + { + Format: dummyFormat("spdx-json"), + Path: "test-3/2.json", + }, + }, + expected: []writerConfig{ + { + format: "json", + file: "test-3/1.json", + }, + { + format: "spdx-json", + file: "test-3/2.json", + }, + }, + }, + { + outputs: []sbomWriterDescription{ + { + Format: dummyFormat("text"), + }, + { + Format: dummyFormat("spdx-json"), + Path: "test-4.json", + }, + }, + expected: []writerConfig{ + { + format: "text", + }, + { + format: "spdx-json", + file: "test-4.json", + }, + }, + }, + } + + for _, test := range tests { + t.Run(testName(test.outputs, test.err), func(t *testing.T) { + outputs := test.outputs + for i := range outputs { + if outputs[i].Path != "" { + outputs[i].Path = tmp + outputs[i].Path + } + } + + mw, err := newSBOMMultiWriter(outputs...) + + if test.err { + assert.Error(t, err) + return + } else { + assert.NoError(t, err) + } + + assert.Len(t, mw.writers, len(test.expected)) + + for i, e := range test.expected { + switch w := mw.writers[i].(type) { + case *sbomStreamWriter: + assert.Equal(t, string(w.format.ID()), e.format) + if e.file != "" { + assert.NotNil(t, w.out) + } else { + assert.NotNil(t, w.out) + } + if e.file != "" { + assert.FileExists(t, tmp+e.file) + } + default: + t.Fatalf("unknown writer type: %T", w) + } + + } + }) + } +} + +func Test_newSBOMWriterDescription(t *testing.T) { + tests := []struct { + name string + path string + expected string + }{ + { + name: "expand home dir", + path: "~/place.txt", + expected: filepath.Join(homedir.Get(), "place.txt"), + }, + { + name: "passthrough other paths", + path: "/other/place.txt", + expected: "/other/place.txt", + }, + { + name: "no path", + path: "", + expected: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := newSBOMWriterDescription(dummyFormat("table"), tt.path) + assert.Equal(t, tt.expected, o.Path) + }) + } +} diff --git a/cmd/syft/cli/packages/packages.go b/cmd/syft/cli/packages/packages.go index 12695e4f086..544a1b502b6 100644 --- a/cmd/syft/cli/packages/packages.go +++ b/cmd/syft/cli/packages/packages.go @@ -12,7 +12,6 @@ import ( "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/config" - "github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/ui" "github.com/anchore/syft/internal/version" "github.com/anchore/syft/syft" @@ -29,17 +28,11 @@ func Run(_ context.Context, app *config.Application, args []string) error { return err } - writer, err := options.MakeWriter(app.Outputs, app.File, app.OutputTemplatePath) + writer, err := options.MakeSBOMWriter(app.Outputs, app.File, app.OutputTemplatePath) if err != nil { return err } - defer func() { - if err := writer.Close(); err != nil { - log.Warnf("unable to write to report destination: %w", err) - } - }() - // could be an image or a directory, with or without a scheme userInput := args[0] si, err := source.ParseInputWithNameVersion(userInput, app.Platform, app.SourceName, app.SourceVersion, app.DefaultImagePullSource) diff --git a/cmd/syft/cli/poweruser/poweruser.go b/cmd/syft/cli/poweruser/poweruser.go index b4e524feadd..724f9a81fa2 100644 --- a/cmd/syft/cli/poweruser/poweruser.go +++ b/cmd/syft/cli/poweruser/poweruser.go @@ -15,7 +15,6 @@ import ( "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/bus" "github.com/anchore/syft/internal/config" - "github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/ui" "github.com/anchore/syft/internal/version" "github.com/anchore/syft/syft" @@ -28,19 +27,11 @@ import ( func Run(_ context.Context, app *config.Application, args []string) error { f := syftjson.Format() - writer, err := sbom.NewWriter(sbom.WriterOption{ - Format: f, - Path: app.File, - }) + writer, err := options.MakeSBOMWriterForFormat(f, app.File) if err != nil { return err } - defer func() { - if err := writer.Close(); err != nil { - log.Warnf("unable to write to report destination: %+v", err) - } - // inform user at end of run that command will be removed deprecated := color.Style{color.Red, color.OpBold}.Sprint("DEPRECATED: This command will be removed in v1.0.0") fmt.Fprintln(os.Stderr, deprecated) diff --git a/syft/lib.go b/syft/lib.go index 7f37013014b..ea28690066a 100644 --- a/syft/lib.go +++ b/syft/lib.go @@ -56,13 +56,13 @@ func CatalogPackages(src *source.Source, cfg cataloger.Config) (*pkg.Collection, // otherwise conditionally use the correct set of loggers based on the input type (container image or directory) switch src.Metadata.Scheme { case source.ImageScheme: - log.Info("cataloging image") + log.Info("cataloging an image") catalogers = cataloger.ImageCatalogers(cfg) case source.FileScheme: - log.Info("cataloging file") + log.Info("cataloging a file") catalogers = cataloger.AllCatalogers(cfg) case source.DirectoryScheme: - log.Info("cataloging directory") + log.Info("cataloging a directory") catalogers = cataloger.DirectoryCatalogers(cfg) default: return nil, nil, nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", src.Metadata.Scheme) diff --git a/syft/sbom/multi_writer.go b/syft/sbom/multi_writer.go deleted file mode 100644 index 6eb85ef0600..00000000000 --- a/syft/sbom/multi_writer.go +++ /dev/null @@ -1,123 +0,0 @@ -package sbom - -import ( - "fmt" - "os" - "path" - - "github.com/hashicorp/go-multierror" - "github.com/mitchellh/go-homedir" - - "github.com/anchore/syft/internal/log" -) - -// multiWriter holds a list of child sbom.Writers to apply all Write and Close operations to -type multiWriter struct { - writers []Writer -} - -// WriterOption Format and path strings used to create sbom.Writer -type WriterOption struct { - Format Format - Path string -} - -func NewWriterOption(f Format, p string) WriterOption { - expandedPath, err := homedir.Expand(p) - if err != nil { - log.Warnf("could not expand given writer output path=%q: %w", p, err) - // ignore errors - expandedPath = p - } - return WriterOption{ - Format: f, - Path: expandedPath, - } -} - -// NewWriter create all report writers from input options; if a file is not specified, os.Stdout is used -func NewWriter(options ...WriterOption) (_ Writer, err error) { - if len(options) == 0 { - return nil, fmt.Errorf("no output options provided") - } - - out := &multiWriter{} - - defer func() { - if err != nil { - // close any previously opened files; we can't really recover from any errors - if err := out.Close(); err != nil { - log.Warnf("unable to close sbom writers: %+v", err) - } - } - }() - - for _, option := range options { - switch len(option.Path) { - case 0: - out.writers = append(out.writers, &streamWriter{ - format: option.Format, - out: os.Stdout, - }) - default: - // create any missing subdirectories - dir := path.Dir(option.Path) - if dir != "" { - s, err := os.Stat(dir) - if err != nil { - err = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ? - if err != nil { - return nil, err - } - } else if !s.IsDir() { - return nil, fmt.Errorf("output path does not contain a valid directory: %s", option.Path) - } - } - fileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - if err != nil { - return nil, fmt.Errorf("unable to create report file: %w", err) - } - out.writers = append(out.writers, &streamWriter{ - format: option.Format, - out: fileOut, - close: fileOut.Close, - }) - } - } - - return out, nil -} - -// Write writes the SBOM to all writers -func (m *multiWriter) Write(s SBOM) (errs error) { - for _, w := range m.writers { - err := w.Write(s) - if err != nil { - errs = multierror.Append(errs, err) - } - } - return errs -} - -// Bytes returns the bytes of the SBOM that would be written -func (m *multiWriter) Bytes(s SBOM) (bytes []byte, err error) { - for _, w := range m.writers { - b, err := w.Bytes(s) - if err != nil { - return nil, err - } - bytes = append(bytes, b...) - } - return bytes, nil -} - -// Close closes all writers -func (m *multiWriter) Close() (errs error) { - for _, w := range m.writers { - err := w.Close() - if err != nil { - errs = multierror.Append(errs, err) - } - } - return errs -} diff --git a/syft/sbom/multi_writer_test.go b/syft/sbom/multi_writer_test.go deleted file mode 100644 index b46e852fc48..00000000000 --- a/syft/sbom/multi_writer_test.go +++ /dev/null @@ -1,204 +0,0 @@ -package sbom - -import ( - "io" - "path/filepath" - "strings" - "testing" - - "github.com/docker/docker/pkg/homedir" - "github.com/stretchr/testify/assert" -) - -func dummyEncoder(io.Writer, SBOM) error { - return nil -} - -func dummyFormat(name string) Format { - return NewFormat(AnyVersion, dummyEncoder, nil, nil, FormatID(name)) -} - -type writerConfig struct { - format string - file string -} - -func TestOutputWriter(t *testing.T) { - tmp := t.TempDir() - - testName := func(options []WriterOption, err bool) string { - var out []string - for _, opt := range options { - out = append(out, string(opt.Format.ID())+"="+opt.Path) - } - errs := "" - if err { - errs = "(err)" - } - return strings.Join(out, ", ") + errs - } - - tests := []struct { - outputs []WriterOption - err bool - expected []writerConfig - }{ - { - outputs: []WriterOption{}, - err: true, - }, - { - outputs: []WriterOption{ - { - Format: dummyFormat("table"), - Path: "", - }, - }, - expected: []writerConfig{ - { - format: "table", - }, - }, - }, - { - outputs: []WriterOption{ - { - Format: dummyFormat("json"), - }, - }, - expected: []writerConfig{ - { - format: "json", - }, - }, - }, - { - outputs: []WriterOption{ - { - Format: dummyFormat("json"), - Path: "test-2.json", - }, - }, - expected: []writerConfig{ - { - format: "json", - file: "test-2.json", - }, - }, - }, - { - outputs: []WriterOption{ - { - Format: dummyFormat("json"), - Path: "test-3/1.json", - }, - { - Format: dummyFormat("spdx-json"), - Path: "test-3/2.json", - }, - }, - expected: []writerConfig{ - { - format: "json", - file: "test-3/1.json", - }, - { - format: "spdx-json", - file: "test-3/2.json", - }, - }, - }, - { - outputs: []WriterOption{ - { - Format: dummyFormat("text"), - }, - { - Format: dummyFormat("spdx-json"), - Path: "test-4.json", - }, - }, - expected: []writerConfig{ - { - format: "text", - }, - { - format: "spdx-json", - file: "test-4.json", - }, - }, - }, - } - - for _, test := range tests { - t.Run(testName(test.outputs, test.err), func(t *testing.T) { - outputs := test.outputs - for i := range outputs { - if outputs[i].Path != "" { - outputs[i].Path = tmp + outputs[i].Path - } - } - - writer, err := NewWriter(outputs...) - - if test.err { - assert.Error(t, err) - return - } else { - assert.NoError(t, err) - } - - mw := writer.(*multiWriter) - - assert.Len(t, mw.writers, len(test.expected)) - - for i, e := range test.expected { - w := mw.writers[i].(*streamWriter) - - assert.Equal(t, string(w.format.ID()), e.format) - - if e.file != "" { - assert.FileExists(t, tmp+e.file) - } - - if e.file != "" { - assert.NotNil(t, w.out) - assert.NotNil(t, w.close) - } else { - assert.NotNil(t, w.out) - assert.Nil(t, w.close) - } - } - }) - } -} - -func TestNewWriterOption(t *testing.T) { - tests := []struct { - name string - path string - expected string - }{ - { - name: "expand home dir", - path: "~/place.txt", - expected: filepath.Join(homedir.Get(), "place.txt"), - }, - { - name: "passthrough other paths", - path: "/other/place.txt", - expected: "/other/place.txt", - }, - { - name: "no path", - path: "", - expected: "", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - o := NewWriterOption(dummyFormat("table"), tt.path) - assert.Equal(t, tt.expected, o.Path) - }) - } -} diff --git a/syft/sbom/stream_writer.go b/syft/sbom/stream_writer.go deleted file mode 100644 index 85a91bdabec..00000000000 --- a/syft/sbom/stream_writer.go +++ /dev/null @@ -1,36 +0,0 @@ -package sbom - -import ( - "bytes" - "io" -) - -// streamWriter implements sbom.Writer for a given format and io.Writer, also providing a close function for cleanup -type streamWriter struct { - format Format - out io.Writer - close func() error -} - -// Write the provided SBOM to the data stream -func (w *streamWriter) Write(s SBOM) error { - return w.format.Encode(w.out, s) -} - -// Bytes returns the bytes of the SBOM that would be written -func (w *streamWriter) Bytes(s SBOM) ([]byte, error) { - var buffer bytes.Buffer - err := w.format.Encode(&buffer, s) - if err != nil { - return nil, err - } - return buffer.Bytes(), nil -} - -// Close any resources, such as open files -func (w *streamWriter) Close() error { - if w.close != nil { - return w.close() - } - return nil -} diff --git a/syft/sbom/writer.go b/syft/sbom/writer.go index 4766714ca9b..272ed4019a0 100644 --- a/syft/sbom/writer.go +++ b/syft/sbom/writer.go @@ -1,16 +1,6 @@ package sbom -import "io" - -// Writer an interface to write SBOMs +// Writer an interface to write SBOMs to a destination type Writer interface { - // Write writes the provided SBOM Write(SBOM) error - - // Bytes returns the bytes of the SBOM that would be written - Bytes(SBOM) ([]byte, error) - - // Closer a resource cleanup hook which will be called after SBOM - // is written or if an error occurs before Write is called - io.Closer } From 7943c73d3f26d731ee5d41344f811f2f4d1c0859 Mon Sep 17 00:00:00 2001 From: Stephane Rufer <1128559+rufman@users.noreply.github.com> Date: Fri, 23 Jun 2023 09:40:46 -0700 Subject: [PATCH 09/11] fix: add support for Dart SDK package dependencies (#1891) Signed-off-by: Stephane Rufer <1128559+rufman@users.noreply.github.com> --- syft/pkg/cataloger/dart/parse_pubspec_lock.go | 19 ++++++++++++++++++- .../cataloger/dart/parse_pubspec_lock_test.go | 13 +++++++++++++ .../cataloger/dart/test-fixtures/pubspec.lock | 5 +++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/syft/pkg/cataloger/dart/parse_pubspec_lock.go b/syft/pkg/cataloger/dart/parse_pubspec_lock.go index 3493f8d1df8..6de98c44a46 100644 --- a/syft/pkg/cataloger/dart/parse_pubspec_lock.go +++ b/syft/pkg/cataloger/dart/parse_pubspec_lock.go @@ -5,7 +5,7 @@ import ( "net/url" "sort" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/syft/artifact" @@ -38,6 +38,23 @@ type pubspecLockDescription struct { ResolvedRef string `yaml:"resolved-ref" mapstructure:"resolved-ref"` } +func (p *pubspecLockDescription) UnmarshalYAML(value *yaml.Node) error { + type pld pubspecLockDescription + var p2 pld + + if value.Decode(&p.Name) == nil { + return nil + } + + if err := value.Decode(&p2); err != nil { + return err + } + + *p = pubspecLockDescription(p2) + + return nil +} + func parsePubspecLock(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { var pkgs []pkg.Package diff --git a/syft/pkg/cataloger/dart/parse_pubspec_lock_test.go b/syft/pkg/cataloger/dart/parse_pubspec_lock_test.go index a5a972e80eb..6db99863d57 100644 --- a/syft/pkg/cataloger/dart/parse_pubspec_lock_test.go +++ b/syft/pkg/cataloger/dart/parse_pubspec_lock_test.go @@ -79,6 +79,19 @@ func TestParsePubspecLock(t *testing.T) { Version: "1.6.0", }, }, + { + Name: "flutter", + Version: "0.0.0", + PURL: "pkg:pub/flutter@0.0.0", + Locations: fixtureLocationSet, + Language: pkg.Dart, + Type: pkg.DartPubPkg, + MetadataType: pkg.DartPubMetadataType, + Metadata: pkg.DartPubMetadata{ + Name: "flutter", + Version: "0.0.0", + }, + }, { Name: "key_binder", Version: "1.11.20", diff --git a/syft/pkg/cataloger/dart/test-fixtures/pubspec.lock b/syft/pkg/cataloger/dart/test-fixtures/pubspec.lock index c7b912f1aef..da464c79527 100644 --- a/syft/pkg/cataloger/dart/test-fixtures/pubspec.lock +++ b/syft/pkg/cataloger/dart/test-fixtures/pubspec.lock @@ -36,6 +36,11 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" key_binder: dependency: "direct main" description: From 38b47e484cef522563860e145d499026dde691e4 Mon Sep 17 00:00:00 2001 From: "anchore-actions-token-generator[bot]" <102182147+anchore-actions-token-generator[bot]@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:58:17 -0400 Subject: [PATCH 10/11] chore(deps): update bootstrap tools to latest versions (#1894) Signed-off-by: GitHub Co-authored-by: spiffcs --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index eb1d67e0bc4..27d37846910 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ BOUNCER_VERSION := v0.4.0 CHRONICLE_VERSION := v0.6.0 GORELEASER_VERSION := v1.18.2 YAJSV_VERSION := v1.4.1 -COSIGN_VERSION := v2.0.2 +COSIGN_VERSION := v2.1.0 QUILL_VERSION := v0.2.0 GLOW_VERSION := v1.5.1 From 0d4f19043e2746528f25ae9429e471217cde95df Mon Sep 17 00:00:00 2001 From: "anchore-actions-token-generator[bot]" <102182147+anchore-actions-token-generator[bot]@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:58:44 -0400 Subject: [PATCH 11/11] chore(deps): update stereoscope to 8c7173ebcf69187d480d4d8b0c6cafaa7aef7024 (#1890) Signed-off-by: GitHub Co-authored-by: kzantow --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 1f1e509b966..1c6e0b49971 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/Masterminds/sprig/v3 v3.2.3 github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8 - github.com/anchore/stereoscope v0.0.0-20230609190519-5b5049bf4d3a + github.com/anchore/stereoscope v0.0.0-20230622163731-8c7173ebcf69 github.com/dave/jennifer v1.6.1 github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da github.com/docker/docker v24.0.2+incompatible diff --git a/go.sum b/go.sum index e7309e92a8c..952c0f1041b 100644 --- a/go.sum +++ b/go.sum @@ -98,8 +98,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 h1:AV7qjwMcM4r8wFhJq3jLRztew3ywIyPTRapl2T1s9o8= github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4= -github.com/anchore/stereoscope v0.0.0-20230609190519-5b5049bf4d3a h1:+Vu04kYx8/4W5WcLeg+HjTTYNNvVMEsXmW9lydK0Rz4= -github.com/anchore/stereoscope v0.0.0-20230609190519-5b5049bf4d3a/go.mod h1:0LsgHgXO4QFnk2hsYwtqd3fR18PIZXlFLIl2qb9tu3g= +github.com/anchore/stereoscope v0.0.0-20230622163731-8c7173ebcf69 h1:ph9Tdh5v0WDMhu2z7fugChvCc4uOX7zu6LaMh5UBgng= +github.com/anchore/stereoscope v0.0.0-20230622163731-8c7173ebcf69/go.mod h1:0LsgHgXO4QFnk2hsYwtqd3fR18PIZXlFLIl2qb9tu3g= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=