Compare commits

...
Author SHA1 Message Date
Alex Goodman f2aa4caf55 [wip] git-root config search concept
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-30 16:43:41 -05:00
Alex Goodman fe616acd98 directory resolver should account for the proc cwd relative to the root path (#644)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-30 15:34:37 -05:00
Blaize KayeandAlex Goodman 6af132e088 Adds composer installed.json parser (#643)
* Adds installed.json functionality and tests

Signed-off-by: Blaize Kaye <blaize.kaye@amazee.com>

* Adds php-installed-cataloger

Signed-off-by: Blaize Kaye <blaize.kaye@amazee.com>

* Changes fallback logic

Signed-off-by: Blaize Kaye <blaize.kaye@amazee.com>

* Adds image tests for installed.json composer packages

Signed-off-by: Blaize Kaye <blaize.kaye@amazee.com>

* tweak PHP cataloger names

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

* unexport PHP types and fix CLI tests

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

* rename PHP cataloger file

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

Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-30 11:36:08 -05:00
Alex Goodman 00206233e6 use smaller jotframe event buffer (#647)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-30 11:23:16 -05:00
Alex Goodman bd9007fc0e Migrate SPDX-JSON relationships to SBOM model (#634)
* remove power-user document shape

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

* add power-user specific fields to syft-json format

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

* port remaining spdx-json relationships to sbom model

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

* add coordinate set

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

* add SBOM file path helper

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

* use internal mimetype helper in go binary cataloger

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

* add new package-of relationship

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

* update json schema to v2

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

* replace power-user presenter with syft-json format

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

* fix tests and linting

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

* remove "package-of" relationship (in favor of "contains")

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

* add tests for spdx22json format encoding enhancements

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

* update TODO and log entries

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

* introduce sbom.Descriptor

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-23 14:54:17 -05:00
Alex Goodman e3b34813d7 ignore partybus unsubscribe errors (#641)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-22 16:07:55 -05:00
Dan Luhring f0b2d81599 Announce meetup on 2021-12-01 (#640)
Signed-off-by: Dan Luhring <dan+github@luhrings.com>
2021-11-22 10:13:18 -05:00
Christopher Angelo Phillips 0849539729 Update concurrency primitives for relationships in command layer (#639)
* update concurrency primitives for relationships in command layer

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-11-20 03:44:17 -05:00
Christopher Angelo Phillips 4f0099583a promote catalog task pattern to all commands (#636)
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-11-19 13:26:23 -05:00
Alex Goodman d76c868481 Catalog archive contents for single-file input (#637)
* add first-level archive processing when input is a file

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

* add license exception for github.com/xi2/xz

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

* always return cleanup function

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

* change source.NewFromFile log entry to warn

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

* ensure file source always has cleanup function

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

* ensure we are always preferring the unarchive cleanup function for source

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-19 14:16:25 +00:00
Alex GoodmanandDan Luhring e38cde35ed Introduce minimal source coordinates (#623)
* split source.Location and create source.Coordinates for minimal path addressing

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

* move coordinates into separate file

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

* Update syft/source/coordinates.go

Co-authored-by: Dan Luhring <luhring@users.noreply.github.com>
2021-11-18 18:13:22 +00:00
Alex Goodman 9090c3a772 bump stereoscope, docker, and GGCR (#633)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-16 19:43:46 +00:00
Alex Goodman ef627d82ef Introduce relationships as first-class objects (#607)
* migrate pkg.ID and pkg.Relationship to artifact package

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

* return relationships from tasks

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

* fix more tests

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

* add artifact.Identifiable by Identity() method

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

* fix linting

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

* remove catalog ID assignment

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

* adjust spdx helpers to use copy of packages

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

* stabilize package ID relative to encode-decode format cycles

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

* rename Identity() to ID()

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

* use zero value for nils in ID generation

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

* enable source.Location to be identifiable

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

* hoist up package relationship discovery to analysis stage

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

* update ownership-by-file-overlap relationship description

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

* add test reminders to put new relationships under test

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

* adjust PHP composer.lock parser function to return relationships

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-16 14:14:13 -05:00
Christopher Angelo Phillips 45ea4177e8 update SPDX license list => 3.15 (#630)
* update SPDX license list 

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-11-15 14:41:46 -05:00
Alex Goodman e28b7b53b0 bump chronicle to v0.3.0 (#627)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-13 18:56:25 -08:00
Christopher Angelo Phillips fc39710d06 287 - Add retry for image fetch using user's input (#626)
* add fallback to user input if source hint fails

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* refactor for smaller functions

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-11-12 17:57:04 -05:00
Swathi Gangisetty e732f419f8 Add new distro and mapping to support Rocky Linux distro identification (#624)
Signed-off-by: Swathi Gangisetty <swathi@anchore.com>
2021-11-12 13:10:21 -08:00
NikitaandAlex Goodman 25835ef589 PHP parser composer.lock (#609)
* PHP parse composer.lock

Signed-off-by: Nikita <33390074+Zilborg@users.noreply.github.com>

* rename PHP package type

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

* add PHP composer package support to source info SPDX helper

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

* update directory cataloger integration tests (make exception for images)

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

Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-10 14:08:41 -05:00
Christopher Angelo Phillips 319c6ee2eb document current draft for iana submission (#618)
* document current draft for IANA submission

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-11-10 11:28:47 -05:00
Alex Goodman 9c27fa7b0b use sbom.SBOM in ImportConfig (#621)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-08 18:39:06 -05:00
Christopher Angelo PhillipsandAlex Goodman 9bbc9ff633 Add layer information for go-module-binary-cataloger (#620)
* update functions to pass Location

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* update unit tests to pass new locations

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* fix image source.FileResolvers to include layer info

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

* add non-empty location in golang binary cataloger testing

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

Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-08 16:05:53 -05:00
Alex GoodmanandChristopher Angelo Phillips bb0f35bac4 Introduce a single SBOM document (#606)
* [wip] single sbom doc

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

* fix tests

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

* fix more tests

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

* fix linting

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

* update cli tests

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

* remove scope in import path

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

* swap SPDX tag-value formatter to single sbom document

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

* bust CLI cache

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

* update fixture to byte diff

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* byte for byte

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* bust the cache

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* who needs cache

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* add jar for testing

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* no more bit flips

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* update apk with the delta for image and directory cases

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* restore cache workflow

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

Co-authored-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-11-05 10:05:49 -04:00
Jonas Galvão Xavier 6d0ee326d8 ignore irregular files while indexing directory (#616)
* stop hidding  command from help doc

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* do not index irregular files

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* fix fixture dir and err name

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* improve the description of irregular files

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>

* explicitly check indexed file name

Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>
2021-11-04 11:24:05 -07:00
Alex Goodman 23a67dd729 document file scheme (#613)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-02 19:47:25 -04:00
Christopher Angelo Phillips 3e20edee55 update readme with private registry section (#610) 2021-11-02 15:00:56 -04:00
Alex Goodman 8ec3f1d102 Use anchore fork of go-presenter (#611)
* use anchore fork of go-presenter

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

* drop coverage threshold

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-02 18:09:26 +00:00
Alex Goodman ecab44bba1 Add SPDX tag-value format object (#605)
* add new spdx tag-value format

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

* remove public presenter package

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-02 13:45:34 -04:00
Alex GoodmanandChristopher Angelo Phillips 484284706c Use named pipe bit when checking for piped input (#603)
* use named pipe bit when checking for piped input

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
Co-authored-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-11-02 16:41:02 +00:00
Alex Goodman 3ac95ac4f6 Allow for cataloging a single file (#608)
* allow for cataloging a single file

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

* use all catalogers for file schemes

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-11-02 12:09:06 -04:00
Christopher Angelo Phillips a2882ee810 Update package identifier to be constant across multiple syft run (#595)
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-29 16:00:36 +00:00
Alex Goodman 358b3a2cf8 port text presenter to a format object (#604)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-29 11:31:15 -04:00
Alex Goodman 9aca23f766 Add SPDX JSON format object (#584)
* remove existing spdxjson presenter + helpers

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

* add new spdx22json format

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

* add common sdpxhelpers (migrated)

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

* use new common spdx helpers

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

* wire up new spdx22json format object

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

* remove lossless syft-specific property bags

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

* remove spdxjson decoder and validator

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

* add nil checks in spdx test helpers

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

* remove empty default case

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

* use explicit golden snapshot

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-29 14:55:20 +00:00
Alex Goodman 4a2d1d7225 Port cyclonedx presenter to format object (#589)
* add new cyclonedx format object

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

* remove cyclonedx presenter

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

* remove cyclonedx presenter call

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

* remove dependence on golden images for format tests

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

* wire up new formt + rename all-presenters ref

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

* add CLI test to ensure that all formats can be expressed as report output

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

* add cyclonedx version and encoding format to package name

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

* optionally preserve format snapshot images

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

* fix linting + text unit tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-29 10:17:03 -04:00
Christopher Angelo Phillips 3286a4d4cc update docker manifest tagging (#600)
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-28 15:41:39 -04:00
Christopher Angelo Phillips 0ff7013382 update manifest to use Tag over Version
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-28 15:05:34 -04:00
Christopher Angelo Phillips 71708e5bf8 update goreleaser with correct tag information (#598)
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-28 13:37:22 -04:00
Jonas Galvão Xavier dd024298eb stop hidding command from help doc (#596)
Signed-off-by: Jonas Galvão Xavier <jonas.agx@gmail.com>
2021-10-27 13:49:04 -07:00
Christopher Angelo Phillips 1c63943055 Add arm64 image support and Darwin M1 support to .goreleaser.yaml (#591)
* update support arm64

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* small update syntax

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* restore release command

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* add docker manifests

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-26 09:42:35 -04:00
Alex Goodman ff5fa2dc1d pull in space suffix fix (#587)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-25 10:14:17 -04:00
Alex Goodman fb588ff500 replace table presenter with format object (#586)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-24 08:09:27 -04:00
Alex Goodman d5b425e1b5 force exiting the etui upon worker error (#583)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-22 13:44:50 -04:00
Dan Luhring 7b319e32bb Remove webinar announcement (#582)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2021-10-22 07:48:36 -04:00
Alex Goodman 89242e6ed3 bump chronicle to v0.2.0-beta (#581)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-21 18:34:17 +00:00
Alex Goodman 4bf08e6b6d swap out the changelog generator for chronicle tool (#580)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-21 13:37:13 -04:00
Alex Goodman 87ce9fff06 swap go binary parsing warning logging entry (#579)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-21 17:03:07 +00:00
Christopher Angelo Phillips 10fa8dc7c9 Add windows support (#548)
* update  build tags, ui support, and stereoscope, and release for windows support

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-21 12:49:36 -04:00
Alex Goodman 560b05c2c9 Introduce new format pattern + port json processing (#550)
* add new format pattern

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

* add syftjson format

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

* add internal formats helper

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

* add SBOM encode/decode to lib API

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

* remove json presenter + update presenter tests to use common utils

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

* remove presenter format enum type + add formats shim in presenter helper

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

* add MustCPE helper for tests

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

* update usage of format enum

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

* add test fixtures for encode/decode tests

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

* fix integration test

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

* migrate format detection to use reader

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

* address review comments

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-20 21:36:34 +00:00
Alex Goodman 5e315c0f17 Disable ETUI for piped input (#571)
* fixed piped input

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

* allow pipedinput helper to raise an error

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

* factor out verbosity check to function

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-20 12:40:52 -04:00
Alex Goodman 6f3fa494d7 Show help text when no args are given (#573)
* show help text when no args are given

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

* repurpose the input args validation function

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

* ensure app does not check for update in cli tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-20 09:51:05 -04:00
Alex GoodmanandChristopher Angelo Phillips a82463574b Rollback goreleaser version (#570)
* rollback goreleaser version

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

* update go sum

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

Co-authored-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-19 12:01:51 -04:00
Alex Goodman cbdb72ea22 remove unnecessary codeql checkout from second parent commit (#567)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-18 13:31:19 -04:00
Christopher Angelo Phillips 7a005fec06 update to point at new script for new release (#551)
* update to point at local script release

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-15 13:26:34 -04:00
Dan Luhring c0f55866ec Announce upcoming webinar (#553)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2021-10-15 12:13:28 -04:00
Christopher Angelo Phillips 9ea6330ebc update split in py package_cataloger to cap at 2 (#549)
Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-14 12:32:06 -04:00
Christopher Angelo Phillips 7d13fbf0d4 540 add additional go package metadata (#546)
* update with distinct golang metadata types

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-13 11:00:28 -04:00
Dan Luhring 10368f6fe8 Add Syft logo (#545)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2021-10-11 21:06:27 +00:00
Josh Bressers f2840029d1 Allow Syft to build with podman (#537)
* Don't check the Built-By flag

Signed-off-by: Josh Bressers <josh@bress.net>

* Remove alpine pinning to resolve conflict with main

Signed-off-by: Josh Bressers <josh@bress.net>
2021-10-11 12:39:34 -04:00
Christopher Angelo Phillips b25f5b664b (464) - remove mod and cargo from image cataloger (#539)
* remove mod and cargo from image cataloger

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* update test error messages for clear failures

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
2021-10-07 15:18:20 -04:00
Christopher Angelo PhillipsandAlex Goodman 3462e18af3 478 identify go binaries and extract mod information (#534)
* add query by MIME type to source.FileResolver

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

* import stereoscope lib changes to find mime type

- add bin cataloger
- add bin parser
- add mime type go utils
- import new resolver

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>

* add go std library code to unpack bin

- keep them in their own (original) files
- add note for "this code was copied from"
- comment the lines the required changing

Signed-off-by: Christopher Angelo Phillips <christopher.phillips@anchore.com>
Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-07 12:16:38 -04:00
Alex Goodman 05ac3f1eff split application configuration parsing to multiple methods (#532)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-06 09:48:53 -04:00
Alex Goodman a000a2926b update linter + fix whitespace (#536)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2021-10-06 13:46:55 +00:00
354 changed files with 12370 additions and 3979 deletions
+5
View File
@@ -15,3 +15,8 @@ ignore-packages:
# * GNU General Public License, version 2.0 or later (GPL-2.0-or-later).
# (we choose Apache-2.0)
- github.com/spdx/tools-golang
# from: https://github.com/xi2/xz/blob/master/LICENSE
# All these files have been put into the public domain.
# You can do whatever you want with these files.
- github.com/xi2/xz
+398
View File
@@ -0,0 +1,398 @@
#!/bin/sh
set -e
# Code generated by godownloader on 2019-12-25T12:47:14Z. DO NOT EDIT.
#
usage() {
this=$1
cat <<EOF
$this: download go binaries for goreleaser/goreleaser
Usage: $this [-b] bindir [-d] [tag]
-b sets bindir or installation directory, Defaults to ./bin
-d turns on debug logging
[tag] is a tag from
https://github.com/goreleaser/goreleaser/releases
If tag is missing, then the latest will be used.
Generated by godownloader
https://github.com/goreleaser/godownloader
EOF
exit 2
}
parse_args() {
#BINDIR is ./bin unless set be ENV
# over-ridden by flag below
BINDIR=${BINDIR:-./bin}
while getopts "b:dh?x" arg; do
case "$arg" in
b) BINDIR="$OPTARG" ;;
d) log_set_priority 10 ;;
h | \?) usage "$0" ;;
x) set -x ;;
esac
done
shift $((OPTIND - 1))
TAG=$1
}
# this function wraps all the destructive operations
# if a curl|bash cuts off the end of the script due to
# network, either nothing will happen or will syntax error
# out preventing half-done work
execute() {
tmpdir=$(mktemp -d)
log_debug "downloading files into ${tmpdir}"
http_download "${tmpdir}/${TARBALL}" "${TARBALL_URL}"
http_download "${tmpdir}/${CHECKSUM}" "${CHECKSUM_URL}"
hash_sha256_verify "${tmpdir}/${TARBALL}" "${tmpdir}/${CHECKSUM}"
srcdir="${tmpdir}"
(cd "${tmpdir}" && untar "${TARBALL}")
test ! -d "${BINDIR}" && install -d "${BINDIR}"
for binexe in $BINARIES; do
if [ "$OS" = "windows" ]; then
binexe="${binexe}.exe"
fi
install "${srcdir}/${binexe}" "${BINDIR}/"
log_info "installed ${BINDIR}/${binexe}"
done
rm -rf "${tmpdir}"
}
get_binaries() {
case "$PLATFORM" in
darwin/386) BINARIES="goreleaser" ;;
darwin/amd64) BINARIES="goreleaser" ;;
darwin/arm64) BINARIES="goreleaser" ;;
darwin/armv6) BINARIES="goreleaser" ;;
linux/386) BINARIES="goreleaser" ;;
linux/amd64) BINARIES="goreleaser" ;;
linux/arm64) BINARIES="goreleaser" ;;
linux/armv6) BINARIES="goreleaser" ;;
windows/386) BINARIES="goreleaser" ;;
windows/amd64) BINARIES="goreleaser" ;;
windows/arm64) BINARIES="goreleaser" ;;
windows/armv6) BINARIES="goreleaser" ;;
*)
log_crit "platform $PLATFORM is not supported. Make sure this script is up-to-date and file request at https://github.com/${PREFIX}/issues/new"
exit 1
;;
esac
}
tag_to_version() {
if [ -z "${TAG}" ]; then
log_info "checking GitHub for latest tag"
else
log_info "checking GitHub for tag '${TAG}'"
fi
REALTAG=$(github_release "$OWNER/$REPO" "${TAG}") && true
if test -z "$REALTAG"; then
log_crit "unable to find '${TAG}' - use 'latest' or see https://github.com/${PREFIX}/releases for details"
exit 1
fi
# if version starts with 'v', remove it
TAG="$REALTAG"
VERSION=${TAG#v}
}
adjust_format() {
# change format (tar.gz or zip) based on OS
case ${OS} in
windows) FORMAT=zip ;;
esac
true
}
adjust_os() {
# adjust archive name based on OS
case ${OS} in
386) OS=i386 ;;
amd64) OS=x86_64 ;;
darwin) OS=Darwin ;;
linux) OS=Linux ;;
windows) OS=Windows ;;
esac
true
}
adjust_arch() {
# adjust archive name based on ARCH
case ${ARCH} in
386) ARCH=i386 ;;
amd64) ARCH=x86_64 ;;
darwin) ARCH=Darwin ;;
linux) ARCH=Linux ;;
windows) ARCH=Windows ;;
esac
true
}
cat /dev/null <<EOF
------------------------------------------------------------------------
https://github.com/client9/shlib - portable posix shell functions
Public domain - http://unlicense.org
https://github.com/client9/shlib/blob/master/LICENSE.md
but credit (and pull requests) appreciated.
------------------------------------------------------------------------
EOF
is_command() {
command -v "$1" >/dev/null
}
echoerr() {
echo "$@" 1>&2
}
log_prefix() {
echo "$0"
}
_logp=6
log_set_priority() {
_logp="$1"
}
log_priority() {
if test -z "$1"; then
echo "$_logp"
return
fi
[ "$1" -le "$_logp" ]
}
log_tag() {
case $1 in
0) echo "emerg" ;;
1) echo "alert" ;;
2) echo "crit" ;;
3) echo "err" ;;
4) echo "warning" ;;
5) echo "notice" ;;
6) echo "info" ;;
7) echo "debug" ;;
*) echo "$1" ;;
esac
}
log_debug() {
log_priority 7 || return 0
echoerr "$(log_prefix)" "$(log_tag 7)" "$@"
}
log_info() {
log_priority 6 || return 0
echoerr "$(log_prefix)" "$(log_tag 6)" "$@"
}
log_err() {
log_priority 3 || return 0
echoerr "$(log_prefix)" "$(log_tag 3)" "$@"
}
log_crit() {
log_priority 2 || return 0
echoerr "$(log_prefix)" "$(log_tag 2)" "$@"
}
uname_os() {
os=$(uname -s | tr '[:upper:]' '[:lower:]')
case "$os" in
cygwin_nt*) os="windows" ;;
mingw*) os="windows" ;;
msys_nt*) os="windows" ;;
esac
echo "$os"
}
uname_arch() {
arch=$(uname -m)
case $arch in
x86_64) arch="amd64" ;;
x86) arch="386" ;;
i686) arch="386" ;;
i386) arch="386" ;;
aarch64) arch="arm64" ;;
armv5*) arch="armv5" ;;
armv6*) arch="armv6" ;;
armv7*) arch="armv7" ;;
esac
echo ${arch}
}
uname_os_check() {
os=$(uname_os)
case "$os" in
darwin) return 0 ;;
dragonfly) return 0 ;;
freebsd) return 0 ;;
linux) return 0 ;;
android) return 0 ;;
nacl) return 0 ;;
netbsd) return 0 ;;
openbsd) return 0 ;;
plan9) return 0 ;;
solaris) return 0 ;;
windows) return 0 ;;
esac
log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib"
return 1
}
uname_arch_check() {
arch=$(uname_arch)
case "$arch" in
386) return 0 ;;
amd64) return 0 ;;
arm64) return 0 ;;
armv5) return 0 ;;
armv6) return 0 ;;
armv7) return 0 ;;
ppc64) return 0 ;;
ppc64le) return 0 ;;
mips) return 0 ;;
mipsle) return 0 ;;
mips64) return 0 ;;
mips64le) return 0 ;;
s390x) return 0 ;;
amd64p32) return 0 ;;
esac
log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib"
return 1
}
untar() {
tarball=$1
case "${tarball}" in
*.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;;
*.tar) tar --no-same-owner -xf "${tarball}" ;;
*.zip) unzip "${tarball}" ;;
*)
log_err "untar unknown archive format for ${tarball}"
return 1
;;
esac
}
http_download_curl() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url")
else
code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url")
fi
if [ "$code" != "200" ]; then
log_debug "http_download_curl received HTTP status $code"
return 1
fi
return 0
}
http_download_wget() {
local_file=$1
source_url=$2
header=$3
if [ -z "$header" ]; then
wget -q -O "$local_file" "$source_url"
else
wget -q --header "$header" -O "$local_file" "$source_url"
fi
}
http_download() {
log_debug "http_download $2"
if is_command curl; then
http_download_curl "$@"
return
elif is_command wget; then
http_download_wget "$@"
return
fi
log_crit "http_download unable to find wget or curl"
return 1
}
http_copy() {
tmp=$(mktemp)
http_download "${tmp}" "$1" "$2" || return 1
body=$(cat "$tmp")
rm -f "${tmp}"
echo "$body"
}
github_release() {
owner_repo=$1
version=$2
test -z "$version" && version="latest"
giturl="https://github.com/${owner_repo}/releases/${version}"
json=$(http_copy "$giturl" "Accept:application/json")
test -z "$json" && return 1
version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//')
test -z "$version" && return 1
echo "$version"
}
hash_sha256() {
TARGET=${1:-/dev/stdin}
if is_command gsha256sum; then
hash=$(gsha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command sha256sum; then
hash=$(sha256sum "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command shasum; then
hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1
echo "$hash" | cut -d ' ' -f 1
elif is_command openssl; then
hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1
echo "$hash" | cut -d ' ' -f a
else
log_crit "hash_sha256 unable to find command to compute sha-256 hash"
return 1
fi
}
hash_sha256_verify() {
TARGET=$1
checksums=$2
if [ -z "$checksums" ]; then
log_err "hash_sha256_verify checksum file not specified in arg2"
return 1
fi
BASENAME=${TARGET##*/}
want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1)
if [ -z "$want" ]; then
log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'"
return 1
fi
got=$(hash_sha256 "$TARGET")
if [ "$want" != "$got" ]; then
log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got"
return 1
fi
}
cat /dev/null <<EOF
------------------------------------------------------------------------
End of functions from https://github.com/client9/shlib
------------------------------------------------------------------------
EOF
PROJECT_NAME="goreleaser"
OWNER=goreleaser
REPO="goreleaser"
BINARY=goreleaser
FORMAT=tar.gz
OS=$(uname_os)
ARCH=$(uname_arch)
PREFIX="$OWNER/$REPO"
# use in logging routines
log_prefix() {
echo "$PREFIX"
}
PLATFORM="${OS}/${ARCH}"
GITHUB_DOWNLOAD=https://github.com/${OWNER}/${REPO}/releases/download
uname_os_check "$OS"
uname_arch_check "$ARCH"
parse_args "$@"
get_binaries
tag_to_version
adjust_format
adjust_os
adjust_arch
log_info "found version: ${VERSION} for ${TAG}/${OS}/${ARCH}"
NAME=${PROJECT_NAME}_${OS}_${ARCH}
TARBALL=${NAME}.${FORMAT}
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
CHECKSUM=checksums.txt
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
execute
log_err "this script is deprecated, please do not use it anymore. check https://github.com/goreleaser/godownloader/issues/207"
-9
View File
@@ -31,15 +31,6 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
-4
View File
@@ -96,10 +96,6 @@ jobs:
steps:
- uses: docker-practice/actions-setup-docker@v1
# note, it is important to always be auth'd into docker.io to prevent rate limiting issues
- name: Login to Docker Hub
run: echo ${{ secrets.TOOLBOX_DOCKER_PASS }} | docker login docker.io -u ${{ secrets.TOOLBOX_DOCKER_USER }} --password-stdin
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
+7
View File
@@ -232,6 +232,9 @@ jobs:
- uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
@@ -305,6 +308,10 @@ jobs:
needs: [Build-Snapshot-Artifacts]
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
- name: Restore go cache
+5 -3
View File
@@ -15,6 +15,7 @@ linters:
- dogsled
- dupl
- errcheck
- exportloopref
- funlen
- gocognit
- goconst
@@ -22,7 +23,6 @@ linters:
- gocyclo
- gofmt
- goimports
- golint
- goprintffuncname
- gosec
- gosimple
@@ -31,8 +31,8 @@ linters:
- misspell
- nakedret
- nolintlint
- revive
- rowserrcheck
- scopelint
- staticcheck
- structcheck
- stylecheck
@@ -49,11 +49,13 @@ linters:
# - godot
# - godox
# - goerr113
# - golint # deprecated
# - gomnd # this is too aggressive
# - interfacer # this is a good idea, but is no longer supported and is prone to false positives
# - lll # without a way to specify per-line exception cases, this is not usable
# - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations
# - nestif
# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code
# - scopelint # deprecated
# - testpackage
# - wsl
# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90)
+64 -10
View File
@@ -12,9 +12,27 @@ builds:
env:
- CGO_ENABLED=0
goos:
# windows not supported yet (due to jotframe)
# - windows
- linux
goarch:
- amd64
- arm64
# Set the modified timestamp on the output binary to the git timestamp (to ensure a reproducible build)
mod_timestamp: '{{ .CommitTimestamp }}'
ldflags: |
-w
-s
-extldflags '-static'
-X github.com/anchore/syft/internal/version.version={{.Version}}
-X github.com/anchore/syft/internal/version.gitCommit={{.Commit}}
-X github.com/anchore/syft/internal/version.buildDate={{.Date}}
-X github.com/anchore/syft/internal/version.gitTreeState={{.Env.BUILD_GIT_TREE_STATE}}
- binary: syft
id: syft-win
env:
- CGO_ENABLED=0
goos:
- windows
goarch:
- amd64
# Set the modified timestamp on the output binary to the git timestamp (to ensure a reproducible build)
@@ -27,7 +45,8 @@ builds:
-X github.com/anchore/syft/internal/version.gitCommit={{.Commit}}
-X github.com/anchore/syft/internal/version.buildDate={{.Date}}
-X github.com/anchore/syft/internal/version.gitTreeState={{.Env.BUILD_GIT_TREE_STATE}}
# For more info on this macOS build, see: https://github.com/mitchellh/gon#usage-with-goreleaser
# For more info on this macOS build, see: https://github.com/mitchellh/gon#usage-with-goreleaser
- binary: syft
id: syft-macos
env:
@@ -36,6 +55,7 @@ builds:
- darwin
goarch:
- amd64
- arm64
# Set the modified timestamp on the output binary to the git timestamp (to ensure a reproducible build)
mod_timestamp: '{{ .CommitTimestamp }}'
ldflags: |
@@ -51,10 +71,11 @@ archives:
- format: tar.gz
builds:
- syft # i.e. Linux only
- format: zip # This is a hack! We don't actually intend to use _this_ ZIP file, we just need goreleaser to consider the ZIP file produced by gon (which will have the same file name) to be an artifact so we can use it downstream in publishing (e.g. to a homebrew tap)
- format: zip # This is a hack for syft-macos! We don't actually intend to use _this_ ZIP file, we just need goreleaser to consider the ZIP file produced by gon (which will have the same file name) to be an artifact so we can use it downstream in publishing (e.g. to a homebrew tap)
id: syft-zip
builds:
- syft-macos
- syft-win
signs:
- artifacts: checksum
@@ -92,15 +113,48 @@ brews:
description: *description
dockers:
- dockerfile: Dockerfile
image_templates:
- image_templates:
- "anchore/syft:latest"
- "anchore/syft:{{ .Tag }}"
- "anchore/syft:v{{ .Major }}"
- "anchore/syft:v{{ .Major }}.{{ .Minor }}"
- "anchore/syft:{{ .Tag }}-amd64"
- "anchore/syft:v{{ .Major }}-amd64"
- "anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64"
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/amd64"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
use_buildx: true
- image_templates:
- "anchore/syft:{{ .Tag }}-arm64v8"
- "anchore/syft:v{{ .Major }}-arm64v8"
- "anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8"
goarch: arm64
dockerfile: Dockerfile
use: buildx
build_flag_templates:
- "--platform=linux/arm64/v8"
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
docker_manifests:
- name_template: anchore/syft:{{ .Tag }}
image_templates:
- anchore/syft:{{ .Tag }}-amd64
- anchore/syft:v{{ .Major }}-amd64
- anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64
- anchore/syft:{{ .Tag }}-arm64v8
- anchore/syft:v{{ .Major }}-arm64v8
- anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8
- name_template: anchore/syft:latest
image_templates:
- anchore/syft:{{ .Tag }}-amd64
- anchore/syft:v{{ .Major }}-amd64
- anchore/syft:v{{ .Major }}.{{ .Minor }}-amd64
- anchore/syft:{{ .Tag }}-arm64v8
- anchore/syft:v{{ .Major }}-arm64v8
- anchore/syft:v{{ .Major }}.{{ .Minor }}-arm64v8
+26 -41
View File
@@ -15,10 +15,10 @@ RESET := $(shell tput -T linux sgr0)
TITLE := $(BOLD)$(PURPLE)
SUCCESS := $(BOLD)$(GREEN)
# the quality gate lower threshold for unit test total % coverage (by function statements)
COVERAGE_THRESHOLD := 65
COVERAGE_THRESHOLD := 62
# CI cache busting values; change these if you want CI to not use previous stored cache
INTEGRATION_CACHE_BUSTER="88738d2f"
CLI_CACHE_BUSTER="789bacdf"
CLI_CACHE_BUSTER="9a2c03cf"
BOOTSTRAP_CACHE="c7afb99ad"
## Build variables
@@ -37,10 +37,6 @@ ifeq "$(strip $(VERSION))" ""
override VERSION = $(shell git describe --always --tags --dirty)
endif
# used to generate the changelog from the second to last tag to the current tag (used in the release pipeline when the release tag is in place)
LAST_TAG := $(shell git describe --abbrev=0 --tags $(shell git rev-list --tags --max-count=1))
SECOND_TO_LAST_TAG := $(shell git describe --abbrev=0 --tags $(shell git rev-list --tags --skip=1 --max-count=1))
## Variable assertions
ifndef TEMPDIR
@@ -101,9 +97,10 @@ $(TEMPDIR):
.PHONY: bootstrap-tools
bootstrap-tools: $(TEMPDIR)
GO111MODULE=off GOBIN=$(shell realpath $(TEMPDIR)) go get -u golang.org/x/perf/cmd/benchstat
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.26.0
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.42.1
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.177.0
curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ v0.3.0
.github/scripts/goreleaser-install.sh -b $(TEMPDIR)/ v0.177.0
.PHONY: bootstrap-go
bootstrap-go:
@@ -138,7 +135,7 @@ lint-fix: ## Auto-format all source code + run golangci lint fixers
go mod tidy
.PHONY: check-licenses
check-licenses:
check-licenses: ## Ensure transitive dependencies are compliant with the current license policy
$(TEMPDIR)/bouncer check
check-go-mod-tidy:
@@ -193,6 +190,11 @@ fixtures:
generate-json-schema: ## Generate a new json schema
cd schema/json && go run generate.go
.PHONY: generate-license-list
generate-license-list: ## Generate an updated spdx license list
go generate ./internal/spdxlicense/...
gofmt -s -w ./internal/spdxlicense
.PHONY: build
build: $(SNAPSHOTDIR) ## Build release snapshot binaries and packages
@@ -203,7 +205,9 @@ $(SNAPSHOTDIR): ## Build snapshot release binaries and packages
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
# build release snapshots
# DOCKER_CLI_EXPERIMENTAL needed to support multi architecture builds for goreleaser
BUILD_GIT_TREE_STATE=$(GITTREESTATE) \
DOCKER_CLI_EXPERIMENTAL=enabled \
$(TEMPDIR)/goreleaser release --skip-publish --skip-sign --rm-dist --snapshot --config $(TEMPDIR)/goreleaser.yaml
# note: we cannot clean the snapshot directory since the pipeline builds the snapshot separately
@@ -249,44 +253,19 @@ cli: $(SNAPSHOTDIR) ## Run CLI tests
SYFT_BINARY_LOCATION='$(SNAPSHOT_CMD)' \
go test -count=1 -v ./test/cli
.PHONY: changlog-release
changelog-release:
@echo "Last tag: $(SECOND_TO_LAST_TAG)"
@docker run --rm \
-v "$(shell pwd)":/usr/local/src/your-app \
ferrarimarco/github-changelog-generator \
--user anchore \
--project $(BIN) \
-t ${GITHUB_TOKEN} \
--exclude-labels 'duplicate,question,invalid,wontfix,size:small,size:medium,size:large,size:x-large' \
--no-pr-wo-labels \
--no-issues-wo-labels \
--since-tag $(SECOND_TO_LAST_TAG)
@printf '\n$(BOLD)$(CYAN)Release $(VERSION) Changelog$(RESET)\n\n'
@cat CHANGELOG.md
.PHONY: changelog-unreleased
changelog-unreleased: ## show the current changelog that will be produced on the next release (note: requires GITHUB_TOKEN set)
@docker run -it --rm \
-v "$(shell pwd)":/usr/local/src/your-app \
ferrarimarco/github-changelog-generator \
--user anchore \
--project $(BIN) \
-t ${GITHUB_TOKEN} \
--exclude-labels 'duplicate,question,invalid,wontfix,size:small,size:medium,size:large,size:x-large' \
--since-tag $(LAST_TAG)
@printf '\n$(BOLD)$(CYAN)Unreleased Changes (closed PRs and issues will not be in the final changelog)$(RESET)\n'
.PHONY: changelog
changelog: clean-changelog CHANGELOG.md
@docker run -it --rm \
-v $(shell pwd)/CHANGELOG.md:/CHANGELOG.md \
rawkode/mdv \
-t 748.5989 \
/CHANGELOG.md
CHANGELOG.md:
$(TEMPDIR)/chronicle -vv > CHANGELOG.md
.PHONY: release
release: clean-dist changelog-release ## Build and publish final binaries and packages. Intended to be run only on macOS.
release: clean-dist CHANGELOG.md ## Build and publish final binaries and packages. Intended to be run only on macOS.
$(call title,Publishing release artifacts)
# Prepare for macOS-specific signing process
@@ -301,9 +280,11 @@ release: clean-dist changelog-release ## Build and publish final binaries and pa
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
# release (note the version transformation from v0.7.0 --> 0.7.0)
# DOCKER_CLI_EXPERIMENTAL needed to support multi architecture builds for goreleaser
bash -c "\
BUILD_GIT_TREE_STATE=$(GITTREESTATE) \
VERSION=$(VERSION:v%=%) \
DOCKER_CLI_EXPERIMENTAL=enabled \
$(TEMPDIR)/goreleaser \
--rm-dist \
--config $(TEMPDIR)/goreleaser.yaml \
@@ -325,9 +306,13 @@ clean-snapshot:
rm -rf $(SNAPSHOTDIR) $(TEMPDIR)/goreleaser.yaml
.PHONY: clean-dist
clean-dist:
clean-dist: clean-changelog
rm -rf $(DISTDIR) $(TEMPDIR)/goreleaser.yaml
.PHONY: clean-changelog
clean-changelog:
rm -f CHANGELOG.md
clean-test-image-cache: clean-test-image-tar-cache clean-test-image-docker-cache
.PHONY: clear-test-image-tar-cache
+96 -7
View File
@@ -1,4 +1,6 @@
# syft
<p align="center">
<img src="https://user-images.githubusercontent.com/5199289/136844524-1527b09f-c5cb-4aa9-be54-5aa92a6086c1.png" width="271" alt="Cute pink owl syft logo">
</p>
[![Validations](https://github.com/anchore/syft/actions/workflows/validations.yaml/badge.svg)](https://github.com/anchore/syft/actions/workflows/validations.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/anchore/syft)](https://goreportcard.com/report/github.com/anchore/syft)
@@ -7,19 +9,21 @@
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/anchore/syft/blob/main/LICENSE)
[![Slack Invite](https://img.shields.io/badge/Slack-Join-blue?logo=slack)](https://anchore.com/slack)
A CLI tool and go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner tool like [Grype](https://github.com/anchore/grype).
A CLI tool and Go library for generating a Software Bill of Materials (SBOM) from container images and filesystems. Exceptional for vulnerability detection when used with a scanner tool like [Grype](https://github.com/anchore/grype).
## We'll be at KubeCon 2021!
### Join us at the virtual OSS meetup!
Attending KubeCon 2021 in person? Join us for a meetup on **Tuesday, October 12th**.
When: December 1, 2021 @ 11am-noon PT (2-3pm ET)
We’ll have free swag, giveaways, snacks, and sips. Space will be limited, so make sure to [save your seat](https://get.anchore.com/2021-kubecon-na-opensource-happy-hour/)!
We’ve got security experts from VMware and Anchore who will share their tips, tricks, and lessons learned on securing containers using [Syft](https://github.com/anchore/syft), [Grype](https://github.com/anchore/grype), and [Cartographer](https://github.com/vmware-tanzu/cartographer).
Register now: https://get.anchore.com/anchore-oss-meetup-dec-1-2021/
---
![syft-demo](https://user-images.githubusercontent.com/590471/90277200-2a253000-de33-11ea-893f-32c219eea11a.gif)
**Features**
## Features
- Catalog container images and filesystems to discover packages and libraries.
- Supports packages and libraries from various ecosystems (APK, DEB, RPM, Ruby Bundles, Python Wheel/Egg/requirements.txt, JavaScript NPM/Yarn, Java JAR/EAR/WAR, Jenkins plugins JPI/HPI, Go modules)
- Linux distribution identification (supports Alpine, BusyBox, CentOS/RedHat, Debian/Ubuntu flavored distributions)
@@ -89,6 +93,7 @@ docker-archive:path/to/yourimage.tar use a tarball from disk for archives crea
oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
dir:path/to/yourproject read directly from a path on disk (any directory)
file:path/to/yourproject/file read directly from a path on disk (any single file)
registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
```
@@ -100,7 +105,7 @@ The output format for Syft is configurable as well:
syft packages <image> -o <format>
```
Where the `format`s available are:
Where the `formats` available are:
- `json`: Use this to get as much information out of Syft as possible!
- `text`: A row-oriented, human-and-machine-friendly output.
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.2 specification](https://cyclonedx.org/specification/overview/).
@@ -108,12 +113,96 @@ Where the `format`s available are:
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
- `table`: A columnar summary (default).
## Private Registry Authentication
### Local Docker Credentials
When a container runtime is not present, Syft can still utilize credentials configured in common credential sources (such as `~/.docker/config.json`).
It will pull images from private registries using these credentials. The config file is where your credentials are stored when authenticating with private registries via some command like `docker login`.
For more information see the `go-containerregistry` [documentation](https://github.com/google/go-containerregistry/tree/main/pkg/authn).
An example `config.json` looks something like this:
```
// config.json
{
"auths": {
"registry.example.com": {
"username": "AzureDiamond",
"password": "hunter2"
}
}
}
```
You can run the following command as an example. It details the mount/environment configuration a container needs to access a private registry:
`docker run -v ./config.json:/config/config.json -e "DOCKER_CONFIG=/config" anchore/syft:latest <private_image>`
### Docker Credentials in Kubernetes
The below section shows a simple workflow on how to mount this config file as a secret into a container on kubernetes.
1. Create a secret. The value of `config.json` is important. It refers to the specification detailed [here](https://github.com/google/go-containerregistry/tree/main/pkg/authn#the-config-file).
Below this section is the `secret.yaml` file that the pod configuration will consume as a volume.
The key `config.json` is important. It will end up being the name of the file when mounted into the pod.
```
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: registry-config
namespace: syft
data:
config.json: <base64 encoded config.json>
```
`kubectl apply -f secret.yaml`
2. Create your pod running syft. The env `DOCKER_CONFIG` is important because it advertises where to look for the credential file.
In the below example, setting `DOCKER_CONFIG=/config` informs syft that credentials can be found at `/config/config.json`.
This is why we used `config.json` as the key for our secret. When mounted into containers the secrets' key is used as the filename.
The `volumeMounts` section mounts our secret to `/config`. The `volumes` section names our volume and leverages the secret we created in step one.
```
# pod.yaml
apiVersion: v1
kind: Pod
spec:
containers:
- image: anchore/syft:latest
name: syft-private-registry-demo
env:
- name: DOCKER_CONFIG
value: /config
volumeMounts:
- mountPath: /config
name: registry-config
readOnly: true
args:
- <private_image>
volumes:
- name: registry-config
secret:
secretName: registry-config
```
`kubectl apply -f pod.yaml`
3. The user can now run `kubectl logs syft-private-registry-demo`. The logs should show the syft analysis for the `<private_image>` provided in the pod configuration.
Using the above information, users should be able to configure private registry access without having to do so in the `grype` or `syft` configuration files.
They will also not be dependent on a docker daemon, (or some other runtime software) for registry configuration and access.
## Configuration
Configuration search paths:
- `.syft.yaml`
- `.syft/config.yaml`
- `<git repo root>/.syft.yaml`
- `<git repo root>/.syft/config.yaml`
- `~/.syft.yaml`
- `<XDG_CONFIG_HOME>/syft/config.yaml`
+8 -2
View File
@@ -48,7 +48,13 @@ func initCmdAliasBindings() {
panic(err)
}
if activeCmd == packagesCmd || activeCmd == rootCmd {
// enable all cataloger by default if power-user command is run
if activeCmd == powerUserCmd {
config.PowerUserCatalogerEnabledDefault()
}
switch activeCmd {
case packagesCmd, rootCmd:
// note: we need to lazily bind config options since they are shared between both the root command
// and the packages command. Otherwise there will be global viper state that is in contention.
// See for more details: https://github.com/spf13/viper/issues/233 . Additionally, the bindings must occur BEFORE
@@ -58,7 +64,7 @@ func initCmdAliasBindings() {
if err = bindPackagesConfigOptions(activeCmd.Flags()); err != nil {
panic(err)
}
} else {
default:
// even though the root command or packages command is NOT being run, we still need default bindings
// such that application config parsing passes.
if err = bindPackagesConfigOptions(packagesCmd.Flags()); err != nil {
+2 -3
View File
@@ -14,9 +14,8 @@ import (
// completionCmd represents the completion command
var completionCmd = &cobra.Command{
Hidden: true,
Use: "completion [bash|zsh|fish]",
Short: "Generate a shell completion for Syft (listing local docker images)",
Use: "completion [bash|zsh|fish]",
Short: "Generate a shell completion for Syft (listing local docker images)",
Long: `To load completions (docker image list):
Bash:
+5 -5
View File
@@ -14,7 +14,7 @@ import (
// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
// an eventual graceful exit.
// nolint:gocognit,funlen
// nolint:funlen
func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error {
defer cleanupFn()
events := subscription.Events()
@@ -41,9 +41,10 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *
if err != nil {
// capture the error from the worker and unsubscribe to complete a graceful shutdown
retErr = multierror.Append(retErr, err)
if err := subscription.Unsubscribe(); err != nil {
retErr = multierror.Append(retErr, err)
}
_ = subscription.Unsubscribe()
// the worker has exited, we may have been mid-handling events for the UI which should now be
// ignored, in which case forcing a teardown of the UI irregardless of the state is required.
forceTeardown = true
}
case e, isOpen := <-events:
if !isOpen {
@@ -53,7 +54,6 @@ func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *
if err := ux.Handle(e); err != nil {
if errors.Is(err, partybus.ErrUnsubscribe) {
log.Warnf("unable to unsubscribe from the event bus")
events = nil
} else {
retErr = multierror.Append(retErr, err)
+88 -43
View File
@@ -10,13 +10,14 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/anchore"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/formats"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/presenter/packages"
"github.com/anchore/syft/syft/format"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/pkg/profile"
"github.com/spf13/cobra"
@@ -43,13 +44,13 @@ const (
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
{{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
{{.appName}} {{.command}} file:path/to/yourproject/file read directly from a path on disk (any single file)
{{.appName}} {{.command}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required)
`
)
var (
packagesPresenterOpt packages.PresenterOption
packagesArgs = cobra.MaximumNArgs(1)
packagesPresenterOpt format.Option
packagesCmd = &cobra.Command{
Use: "packages [SOURCE]",
Short: "Generate a package SBOM",
@@ -58,21 +59,13 @@ var (
"appName": internal.ApplicationName,
"command": "packages",
}),
Args: packagesArgs,
Args: validateInputArgs,
SilenceUsage: true,
SilenceErrors: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
err := cmd.Help()
if err != nil {
return err
}
return fmt.Errorf("an image/directory argument is required")
}
// set the presenter
presenterOption := packages.ParsePresenterOption(appConfig.Output)
if presenterOption == packages.UnknownPresenterOption {
presenterOption := format.ParseOption(appConfig.Output)
if presenterOption == format.UnknownFormatOption {
return fmt.Errorf("bad --output value '%s'", appConfig.Output)
}
packagesPresenterOpt = presenterOption
@@ -102,15 +95,15 @@ func init() {
}
func setPackageFlags(flags *pflag.FlagSet) {
///////// Formatting & Input options //////////////////////////////////////////////
// Formatting & Input options //////////////////////////////////////////////
flags.StringP(
"scope", "s", source.SquashedScope.String(),
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
flags.StringP(
"output", "o", string(packages.TablePresenterOption),
fmt.Sprintf("report output formatter, options=%v", packages.AllPresenters),
"output", "o", string(format.TableOption),
fmt.Sprintf("report output formatter, options=%v", format.AllOptions),
)
flags.StringP(
@@ -118,7 +111,7 @@ func setPackageFlags(flags *pflag.FlagSet) {
"file to write the report output to (default is STDOUT)",
)
///////// Upload options //////////////////////////////////////////////////////////
// Upload options //////////////////////////////////////////////////////////
flags.StringP(
"host", "H", "",
"the hostname or URL of the Anchore Enterprise instance to upload to",
@@ -151,7 +144,7 @@ func setPackageFlags(flags *pflag.FlagSet) {
}
func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
///////// Formatting & Input options //////////////////////////////////////////////
// Formatting & Input options //////////////////////////////////////////////
if err := viper.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
return err
@@ -165,7 +158,7 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
return err
}
///////// Upload options //////////////////////////////////////////////////////////
// Upload options //////////////////////////////////////////////////////////
if err := viper.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
return err
@@ -194,6 +187,18 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
return nil
}
func validateInputArgs(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
// in the case that no arguments are given we want to show the help text and return with a non-0 return code.
if err := cmd.Help(); err != nil {
return fmt.Errorf("unable to display help: %w", err)
}
return fmt.Errorf("an image/directory argument is required")
}
return cobra.MaximumNArgs(1)(cmd, args)
}
func packagesExec(_ *cobra.Command, args []string) error {
// could be an image or a directory, with or without a scheme
userInput := args[0]
@@ -214,15 +219,38 @@ func packagesExec(_ *cobra.Command, args []string) error {
setupSignals(),
eventSubscription,
stereoscope.Cleanup,
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)...,
ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
)
}
func isVerbose() (result bool) {
isPipedInput, err := internal.IsPipedInput()
if err != nil {
// since we can't tell if there was piped input we assume that there could be to disable the ETUI
log.Warnf("unable to determine if there is piped input: %+v", err)
return true
}
// verbosity should consider if there is piped input (in which case we should not show the ETUI)
return appConfig.CliOptions.Verbosity > 0 || isPipedInput
}
func packagesExecWorker(userInput string) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
tasks, err := tasks()
if err != nil {
errs <- err
return
}
f := formats.ByOption(packagesPresenterOpt)
if f == nil {
errs <- fmt.Errorf("unknown format: %s", packagesPresenterOpt)
return
}
checkForApplicationUpdate()
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
@@ -230,35 +258,54 @@ func packagesExecWorker(userInput string) <-chan error {
errs <- fmt.Errorf("failed to determine image source: %w", err)
return
}
defer cleanup()
catalog, d, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
if err != nil {
errs <- fmt.Errorf("failed to catalog input: %w", err)
return
if cleanup != nil {
defer cleanup()
}
s := sbom.SBOM{
Source: src.Metadata,
Descriptor: sbom.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: appConfig,
},
}
var relationships []<-chan artifact.Relationship
for _, task := range tasks {
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
go runTask(task, &s.Artifacts, src, c, errs)
}
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
if appConfig.Anchore.Host != "" {
if err := runPackageSbomUpload(src, src.Metadata, catalog, d, appConfig.Package.Cataloger.ScopeOpt); err != nil {
if err := runPackageSbomUpload(src, s); err != nil {
errs <- err
return
}
}
bus.Publish(partybus.Event{
Type: event.PresenterReady,
Value: packages.Presenter(packagesPresenterOpt, packages.PresenterConfig{
SourceMetadata: src.Metadata,
Catalog: catalog,
Distro: d,
Scope: appConfig.Package.Cataloger.ScopeOpt,
}),
Type: event.PresenterReady,
Value: f.Presenter(s),
})
}()
return errs
}
func runPackageSbomUpload(src *source.Source, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) error {
func mergeRelationships(cs ...<-chan artifact.Relationship) (relationships []artifact.Relationship) {
for _, c := range cs {
for n := range c {
relationships = append(relationships, n)
}
}
return relationships
}
func runPackageSbomUpload(src *source.Source, s sbom.SBOM) error {
log.Infof("uploading results to %s", appConfig.Anchore.Host)
if src.Metadata.Scheme != source.ImageScheme {
@@ -293,17 +340,15 @@ func runPackageSbomUpload(src *source.Source, s source.Metadata, catalog *pkg.Ca
importCfg := anchore.ImportConfig{
ImageMetadata: src.Image.Metadata,
SourceMetadata: s,
Catalog: catalog,
Distro: d,
SBOM: s,
Dockerfile: dockerfileContents,
OverwriteExistingUpload: appConfig.Anchore.OverwriteExistingImage,
Scope: scope,
Timeout: appConfig.Anchore.ImportTimeout,
}
if err := c.Import(context.Background(), importCfg); err != nil {
return fmt.Errorf("failed to upload results to host=%s: %+v", appConfig.Anchore.Host, err)
}
return nil
}
+38 -30
View File
@@ -2,16 +2,20 @@ package cmd
import (
"fmt"
"sync"
"os"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/presenter/poweruser"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/gookit/color"
"github.com/pkg/profile"
"github.com/spf13/cobra"
"github.com/wagoodman/go-partybus"
@@ -19,6 +23,8 @@ import (
const powerUserExample = ` {{.appName}} {{.command}} <image>
DEPRECATED - THIS COMMAND WILL BE REMOVED in v1.0.0
Only image sources are supported (e.g. docker: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported.
All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration)
@@ -35,19 +41,11 @@ var powerUserCmd = &cobra.Command{
"appName": internal.ApplicationName,
"command": "power-user",
}),
Args: cobra.MaximumNArgs(1),
Args: validateInputArgs,
Hidden: true,
SilenceUsage: true,
SilenceErrors: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
err := cmd.Help()
if err != nil {
return err
}
return fmt.Errorf("an image/directory argument is required")
}
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
return fmt.Errorf("cannot profile CPU and memory simultaneously")
}
@@ -80,6 +78,10 @@ func powerUserExec(_ *cobra.Command, args []string) error {
if err := closer(); 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)
}()
if err != nil {
@@ -91,16 +93,19 @@ func powerUserExec(_ *cobra.Command, args []string) error {
setupSignals(),
eventSubscription,
stereoscope.Cleanup,
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet, reporter)...,
ui.Select(isVerbose(), appConfig.Quiet, reporter)...,
)
}
func powerUserExecWorker(userInput string) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
tasks, err := powerUserTasks()
appConfig.Secrets.Cataloger.Enabled = true
appConfig.FileMetadata.Cataloger.Enabled = true
appConfig.FileContents.Cataloger.Enabled = true
appConfig.FileClassification.Cataloger.Enabled = true
tasks, err := tasks()
if err != nil {
errs <- err
return
@@ -113,31 +118,34 @@ func powerUserExecWorker(userInput string) <-chan error {
errs <- err
return
}
defer cleanup()
analysisResults := poweruser.JSONDocumentConfig{
SourceMetadata: src.Metadata,
ApplicationConfig: *appConfig,
if cleanup != nil {
defer cleanup()
}
wg := &sync.WaitGroup{}
s := sbom.SBOM{
Source: src.Metadata,
Descriptor: sbom.Descriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: appConfig,
},
}
var relationships []<-chan artifact.Relationship
for _, task := range tasks {
wg.Add(1)
go func(task powerUserTask) {
defer wg.Done()
if err = task(&analysisResults, src); err != nil {
errs <- err
return
}
}(task)
c := make(chan artifact.Relationship)
relationships = append(relationships, c)
go runTask(task, &s.Artifacts, src, c, errs)
}
wg.Wait()
s.Relationships = append(s.Relationships, mergeRelationships(relationships...)...)
bus.Publish(partybus.Event{
Type: event.PresenterReady,
Value: poweruser.NewJSONPresenter(analysisResults),
Value: syftjson.Format().Presenter(s),
})
}()
return errs
}
+5 -1
View File
@@ -11,18 +11,22 @@ import (
func reportWriter() (io.Writer, func() error, error) {
nop := func() error { return nil }
path := strings.TrimSpace(appConfig.File)
switch len(path) {
case 0:
return os.Stdout, nop, nil
default:
reportFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return nil, nop, fmt.Errorf("unable to create report file: %w", err)
}
return reportFile, func() error {
log.Infof("report written to file=%q", path)
return reportFile.Close()
}, nil
}
+1
View File
@@ -33,6 +33,7 @@ func init() {
flag, "q", false,
"suppress all logging output",
)
if err := viper.BindPFlag(flag, rootCmd.PersistentFlags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
+57 -41
View File
@@ -4,24 +4,25 @@ import (
"crypto"
"fmt"
"github.com/anchore/syft/internal/presenter/poweruser"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
type powerUserTask func(*poweruser.JSONDocumentConfig, *source.Source) error
type task func(*sbom.Artifacts, *source.Source) ([]artifact.Relationship, error)
func powerUserTasks() ([]powerUserTask, error) {
var tasks []powerUserTask
func tasks() ([]task, error) {
var tasks []task
generators := []func() (powerUserTask, error){
catalogPackagesTask,
catalogFileMetadataTask,
catalogFileDigestsTask,
catalogSecretsTask,
catalogFileClassificationsTask,
catalogContentsTask,
generators := []func() (task, error){
generateCatalogPackagesTask,
generateCatalogFileMetadataTask,
generateCatalogFileDigestsTask,
generateCatalogSecretsTask,
generateCatalogFileClassificationsTask,
generateCatalogContentsTask,
}
for _, generator := range generators {
@@ -29,6 +30,7 @@ func powerUserTasks() ([]powerUserTask, error) {
if err != nil {
return nil, err
}
if task != nil {
tasks = append(tasks, task)
}
@@ -37,51 +39,51 @@ func powerUserTasks() ([]powerUserTask, error) {
return tasks, nil
}
func catalogPackagesTask() (powerUserTask, error) {
func generateCatalogPackagesTask() (task, error) {
if !appConfig.Package.Cataloger.Enabled {
return nil, nil
}
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
packageCatalog, theDistro, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
packageCatalog, relationships, theDistro, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
results.PackageCatalog = packageCatalog
results.Distro = theDistro
return nil
return relationships, nil
}
return task, nil
}
func catalogFileMetadataTask() (powerUserTask, error) {
func generateCatalogFileMetadataTask() (task, error) {
if !appConfig.FileMetadata.Cataloger.Enabled {
return nil, nil
}
metadataCataloger := file.NewMetadataCataloger()
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := metadataCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.FileMetadata = result
return nil
return nil, nil
}
return task, nil
}
func catalogFileDigestsTask() (powerUserTask, error) {
func generateCatalogFileDigestsTask() (task, error) {
if !appConfig.FileMetadata.Cataloger.Enabled {
return nil, nil
}
@@ -110,24 +112,24 @@ func catalogFileDigestsTask() (powerUserTask, error) {
return nil, err
}
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := digestsCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.FileDigests = result
return nil
return nil, nil
}
return task, nil
}
func catalogSecretsTask() (powerUserTask, error) {
func generateCatalogSecretsTask() (task, error) {
if !appConfig.Secrets.Cataloger.Enabled {
return nil, nil
}
@@ -142,24 +144,24 @@ func catalogSecretsTask() (powerUserTask, error) {
return nil, err
}
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.Secrets.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := secretsCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.Secrets = result
return nil
return nil, nil
}
return task, nil
}
func catalogFileClassificationsTask() (powerUserTask, error) {
func generateCatalogFileClassificationsTask() (task, error) {
if !appConfig.FileClassification.Cataloger.Enabled {
return nil, nil
}
@@ -170,24 +172,24 @@ func catalogFileClassificationsTask() (powerUserTask, error) {
return nil, err
}
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.FileClassification.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := classifierCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.FileClassifications = result
return nil
return nil, nil
}
return task, nil
}
func catalogContentsTask() (powerUserTask, error) {
func generateCatalogContentsTask() (task, error) {
if !appConfig.FileContents.Cataloger.Enabled {
return nil, nil
}
@@ -197,19 +199,33 @@ func catalogContentsTask() (powerUserTask, error) {
return nil, err
}
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(appConfig.FileContents.Cataloger.ScopeOpt)
if err != nil {
return err
return nil, err
}
result, err := contentsCataloger.Catalog(resolver)
if err != nil {
return err
return nil, err
}
results.FileContents = result
return nil
return nil, nil
}
return task, nil
}
func runTask(t task, a *sbom.Artifacts, src *source.Source, c chan<- artifact.Relationship, errs chan<- error) {
defer close(c)
relationships, err := t(a, src)
if err != nil {
errs <- err
return
}
for _, relationship := range relationships {
c <- relationship
}
}
+23 -14
View File
@@ -7,43 +7,52 @@ require (
github.com/adrg/xdg v0.2.1
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
github.com/anchore/go-presenter v0.0.0-20211102174526-0dbf20f6c7fa
github.com/anchore/go-rpmdb v0.0.0-20210914181456-a9c52348da63
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.0.0-20210922164639-b3fa992ebd29
github.com/anchore/stereoscope v0.0.0-20211005213828-538011008578
github.com/anchore/stereoscope v0.0.0-20211116152349-7e4e1b56a15d
// we are hinting brotli to latest due to warning when installing archiver v3:
// go: warning: github.com/andybalholm/brotli@v1.0.1: retracted by module author: occasional panics and data corruption
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/antihax/optional v1.0.0
github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
github.com/docker/docker v20.10.10+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/facebookincubator/nvdtools v0.1.4
github.com/go-test/deep v1.0.7
github.com/google/uuid v1.1.1
github.com/google/go-cmp v0.5.6
github.com/google/uuid v1.2.0
github.com/gookit/color v1.2.7
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/go-version v1.2.0
github.com/jinzhu/copier v0.3.2
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mholt/archiver/v3 v3.5.1
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.3.1
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/mitchellh/mapstructure v1.4.1
github.com/olekukonko/tablewriter v0.0.4
github.com/pelletier/go-toml v1.8.0
github.com/pelletier/go-toml v1.9.3
github.com/pkg/profile v1.5.0
github.com/scylladb/go-set v1.0.2
github.com/sergi/go-diff v1.1.0
github.com/sirupsen/logrus v1.6.0
github.com/sirupsen/logrus v1.8.1
github.com/spdx/tools-golang v0.1.0
github.com/spf13/afero v1.2.2
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905
github.com/spf13/afero v1.6.0
github.com/spf13/cobra v1.2.1
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.0
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.7.0
github.com/vifraa/gopom v0.1.0
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163
github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb
github.com/x-cray/logrus-prefixed-formatter v0.5.2
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/mod v0.3.0
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
gopkg.in/yaml.v2 v2.3.0
golang.org/x/mod v0.4.2
golang.org/x/net v0.0.0-20211111160137-58aab5ef257a
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
gopkg.in/yaml.v2 v2.4.0
)
+671 -451
View File
File diff suppressed because it is too large Load Diff
+6 -12
View File
@@ -6,27 +6,21 @@ import (
"fmt"
"time"
"github.com/antihax/optional"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/sbom"
"github.com/antihax/optional"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
)
type ImportConfig struct {
ImageMetadata image.Metadata
SourceMetadata source.Metadata
Catalog *pkg.Catalog
Distro *distro.Distro
SBOM sbom.SBOM
Dockerfile []byte
OverwriteExistingUpload bool
Scope source.Scope
Timeout uint
}
@@ -74,19 +68,19 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
prog.N++
sessionID := startOperation.Uuid
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, cfg.SourceMetadata, cfg.Catalog, cfg.Distro, cfg.Scope, stage)
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM, stage)
if err != nil {
return fmt.Errorf("failed to import Package SBOM: %w", err)
}
prog.N++
manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, cfg.ImageMetadata.RawManifest, stage)
manifestDigest, err := importManifest(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Source.ImageMetadata.RawManifest, stage)
if err != nil {
return fmt.Errorf("failed to import Manifest: %w", err)
}
prog.N++
configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, cfg.ImageMetadata.RawConfig, stage)
configDigest, err := importConfig(authedCtx, c.client.ImportsApi, sessionID, cfg.SBOM.Source.ImageMetadata.RawConfig, stage)
if err != nil {
return fmt.Errorf("failed to import Config: %w", err)
}
+8 -10
View File
@@ -8,26 +8,24 @@ import (
"fmt"
"net/http"
"github.com/anchore/syft/internal/presenter/packages"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/source"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
)
type packageSBOMImportAPI interface {
ImportImagePackages(context.Context, string, external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error)
}
func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) (*external.ImagePackageManifest, error) {
func packageSbomModel(s sbom.SBOM) (*external.ImagePackageManifest, error) {
var buf bytes.Buffer
pres := packages.NewJSONPresenter(catalog, s, d, scope)
err := pres.Present(&buf)
err := syftjson.Format().Presenter(s).Present(&buf)
if err != nil {
return nil, fmt.Errorf("unable to serialize results: %w", err)
}
@@ -41,11 +39,11 @@ func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro,
return &model, nil
}
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope, stage *progress.Stage) (string, error) {
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s sbom.SBOM, stage *progress.Stage) (string, error) {
log.Debug("importing package SBOM")
stage.Current = "package SBOM"
model, err := packageSbomModel(s, catalog, d, scope)
model, err := packageSbomModel(s)
if err != nil {
return "", fmt.Errorf("unable to create PackageSBOM model: %w", err)
}
+44 -17
View File
@@ -9,18 +9,17 @@ import (
"strings"
"testing"
"github.com/anchore/syft/internal/presenter/packages"
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/syft/distro"
"github.com/docker/docker/pkg/ioutils"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/client-go/pkg/external"
"github.com/anchore/syft/internal/formats/syftjson"
syftjsonModel "github.com/anchore/syft/internal/formats/syftjson/model"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/docker/docker/pkg/ioutils"
"github.com/go-test/deep"
"github.com/wagoodman/go-progress"
)
func must(c pkg.CPE, e error) pkg.CPE {
@@ -60,8 +59,10 @@ func TestPackageSbomToModel(t *testing.T) {
FoundBy: "foundBy",
Locations: []source.Location{
{
RealPath: "path",
FileSystemID: "layerID",
Coordinates: source.Coordinates{
RealPath: "path",
FileSystemID: "layerID",
},
},
},
Licenses: []string{"license"},
@@ -75,7 +76,15 @@ func TestPackageSbomToModel(t *testing.T) {
c := pkg.NewCatalog(p)
model, err := packageSbomModel(m, c, &d, source.AllLayersScope)
sbomResult := sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: c,
Distro: &d,
},
Source: m,
}
model, err := packageSbomModel(sbomResult)
if err != nil {
t.Fatalf("unable to generate model from source material: %+v", err)
}
@@ -87,20 +96,28 @@ func TestPackageSbomToModel(t *testing.T) {
t.Fatalf("unable to marshal model: %+v", err)
}
s := sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: c,
Distro: &d,
},
Source: m,
}
var buf bytes.Buffer
pres := packages.NewJSONPresenter(c, m, &d, source.AllLayersScope)
pres := syftjson.Format().Presenter(s)
if err := pres.Present(&buf); err != nil {
t.Fatalf("unable to get expected json: %+v", err)
}
// unmarshal expected result
var expectedDoc packages.JSONDocument
var expectedDoc syftjsonModel.Document
if err := json.Unmarshal(buf.Bytes(), &expectedDoc); err != nil {
t.Fatalf("unable to parse json doc: %+v", err)
}
// unmarshal actual result
var actualDoc packages.JSONDocument
var actualDoc syftjsonModel.Document
if err := json.Unmarshal(modelJSON, &actualDoc); err != nil {
t.Fatalf("unable to parse json doc: %+v", err)
}
@@ -142,8 +159,10 @@ func TestPackageSbomImport(t *testing.T) {
FoundBy: "foundBy",
Locations: []source.Location{
{
RealPath: "path",
FileSystemID: "layerID",
Coordinates: source.Coordinates{
RealPath: "path",
FileSystemID: "layerID",
},
},
},
Licenses: []string{"license"},
@@ -190,7 +209,15 @@ func TestPackageSbomImport(t *testing.T) {
d, _ := distro.NewDistro(distro.CentOS, "8.0", "")
theModel, err := packageSbomModel(m, catalog, &d, source.AllLayersScope)
sbomResult := sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: catalog,
Distro: &d,
},
Source: m,
}
theModel, err := packageSbomModel(sbomResult)
if err != nil {
t.Fatalf("could not get sbom model: %+v", err)
}
@@ -229,7 +256,7 @@ func TestPackageSbomImport(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
digest, err := importPackageSBOM(context.TODO(), test.api, sessionID, m, catalog, &d, source.AllLayersScope, &progress.Stage{})
digest, err := importPackageSBOM(context.TODO(), test.api, sessionID, sbomResult, &progress.Stage{})
// validate error handling
if err != nil && !test.expectsError {
+118 -50
View File
@@ -3,7 +3,10 @@ package config
import (
"errors"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"strings"
@@ -17,6 +20,8 @@ import (
var ErrApplicationConfigNotFound = fmt.Errorf("application config not found")
var catalogerEnabledDefault = false
type defaultValueLoader interface {
loadDefaultValues(*viper.Viper)
}
@@ -44,6 +49,11 @@ type Application struct {
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
}
// PowerUserCatalogerEnabledDefault switches all catalogers to be enabled when running power-user command
func PowerUserCatalogerEnabledDefault() {
catalogerEnabledDefault = true
}
func newApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application {
config := &Application{
CliOptions: cliOpts,
@@ -89,42 +99,18 @@ func (cfg Application) loadDefaultValues(v *viper.Viper) {
}
}
// build inflates simple config values into syft native objects (or other complex objects) after the config is fully read in.
func (cfg *Application) parseConfigValues() error {
if cfg.Quiet {
// TODO: this is bad: quiet option trumps all other logging options
// we should be able to quiet the console logging and leave file logging alone...
// ... this will be an enhancement for later
cfg.Log.LevelOpt = logrus.PanicLevel
} else {
if cfg.Log.Level != "" {
if cfg.CliOptions.Verbosity > 0 {
return fmt.Errorf("cannot explicitly set log level (cfg file or env var) and use -v flag together")
}
lvl, err := logrus.ParseLevel(strings.ToLower(cfg.Log.Level))
if err != nil {
return fmt.Errorf("bad log level configured (%q): %w", cfg.Log.Level, err)
}
// set the log level explicitly
cfg.Log.LevelOpt = lvl
} else {
// set the log level implicitly
switch v := cfg.CliOptions.Verbosity; {
case v == 1:
cfg.Log.LevelOpt = logrus.InfoLevel
case v >= 2:
cfg.Log.LevelOpt = logrus.DebugLevel
default:
cfg.Log.LevelOpt = logrus.WarnLevel
}
// parse application config options
for _, optionFn := range []func() error{
cfg.parseUploadOptions,
cfg.parseLogLevelOption,
} {
if err := optionFn(); err != nil {
return err
}
}
if cfg.Anchore.Host == "" && cfg.Anchore.Dockerfile != "" {
return fmt.Errorf("cannot provide dockerfile option without enabling upload")
}
// parse nested config options
// for each field in the configuration struct, see if the field implements the parser interface
// note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address)
value := reflect.ValueOf(cfg).Elem()
@@ -137,6 +123,52 @@ func (cfg *Application) parseConfigValues() error {
}
}
}
return nil
}
func (cfg *Application) parseUploadOptions() error {
if cfg.Anchore.Host == "" && cfg.Anchore.Dockerfile != "" {
return fmt.Errorf("cannot provide dockerfile option without enabling upload")
}
return nil
}
func (cfg *Application) parseLogLevelOption() error {
switch {
case cfg.Quiet:
// TODO: this is bad: quiet option trumps all other logging options (such as to a file on disk)
// we should be able to quiet the console logging and leave file logging alone...
// ... this will be an enhancement for later
cfg.Log.LevelOpt = logrus.PanicLevel
case cfg.Log.Level != "":
if cfg.CliOptions.Verbosity > 0 {
return fmt.Errorf("cannot explicitly set log level (cfg file or env var) and use -v flag together")
}
lvl, err := logrus.ParseLevel(strings.ToLower(cfg.Log.Level))
if err != nil {
return fmt.Errorf("bad log level configured (%q): %w", cfg.Log.Level, err)
}
cfg.Log.LevelOpt = lvl
if cfg.Log.LevelOpt >= logrus.InfoLevel {
cfg.CliOptions.Verbosity = 1
}
default:
switch v := cfg.CliOptions.Verbosity; {
case v == 1:
cfg.Log.LevelOpt = logrus.InfoLevel
case v >= 2:
cfg.Log.LevelOpt = logrus.DebugLevel
default:
cfg.Log.LevelOpt = logrus.ErrorLevel
}
}
if cfg.Log.Level == "" {
cfg.Log.Level = cfg.Log.LevelOpt.String()
}
return nil
}
@@ -177,44 +209,80 @@ func readConfig(v *viper.Viper, configPath string) error {
// 1. look for .<appname>.yaml (in the current directory)
v.AddConfigPath(".")
v.SetConfigName("." + internal.ApplicationName)
if err = v.ReadInConfig(); err == nil {
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
if exists, err := readViperConfig(v); exists || err != nil {
return err
}
// 2. look for .<appname>/config.yaml (in the current directory)
v.AddConfigPath("." + internal.ApplicationName)
v.SetConfigName("config")
if err = v.ReadInConfig(); err == nil {
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
if exists, err := readViperConfig(v); exists || err != nil {
return err
}
// 3. look for ~/.<appname>.yaml
if root := gitRepoRoot(); root != "" {
// 3. look for <git repo root>/.<appname>.yaml, where the repo root is defined by "git rev-parse --show-toplevel"
v.AddConfigPath(root)
v.SetConfigName("." + internal.ApplicationName)
if exists, err := readViperConfig(v); exists {
if err != nil {
return err
}
return os.Chdir(root)
}
// 4. look for <git repo root>/.<appname>/config.yaml, where the repo root is defined by "git rev-parse --show-toplevel"
v.AddConfigPath(filepath.Join(root, "."+internal.ApplicationName))
v.SetConfigName("config")
if exists, err := readViperConfig(v); exists {
if err != nil {
return err
}
return os.Chdir(root)
}
}
// 5. look for ~/.<appname>.yaml
home, err := homedir.Dir()
if err == nil {
v.AddConfigPath(home)
v.SetConfigName("." + internal.ApplicationName)
if err = v.ReadInConfig(); err == nil {
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
if exists, err := readViperConfig(v); exists || err != nil {
return err
}
}
// 4. look for <appname>/config.yaml in xdg locations (starting with xdg home config dir, then moving upwards)
// 6. look for <appname>/config.yaml in xdg locations (starting with xdg home config dir, then moving upwards)
v.AddConfigPath(path.Join(xdg.ConfigHome, internal.ApplicationName))
for _, dir := range xdg.ConfigDirs {
v.AddConfigPath(path.Join(dir, internal.ApplicationName))
}
v.SetConfigName("config")
if err = v.ReadInConfig(); err == nil {
return nil
} else if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
if exists, err := readViperConfig(v); exists || err != nil {
return err
}
return ErrApplicationConfigNotFound
}
func readViperConfig(v *viper.Viper) (bool, error) {
err := v.ReadInConfig()
if err == nil {
return true, nil
} else if errors.As(err, &viper.ConfigFileNotFoundError{}) {
return false, nil
}
return true, fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err)
}
func gitRepoRoot() string {
root, err := exec.Command("git", "rev-parse", "--show-toplevel").Output()
if err != nil {
return ""
}
absRepoRoot, err := filepath.Abs(strings.TrimSpace(string(root)))
if err != nil {
return ""
}
return absRepoRoot
}
+1 -1
View File
@@ -10,7 +10,7 @@ type fileClassification struct {
}
func (cfg fileClassification) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-classification.cataloger.enabled", true)
v.SetDefault("file-classification.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-classification.cataloger.scope", source.SquashedScope)
}
+1 -1
View File
@@ -13,7 +13,7 @@ type fileContents struct {
}
func (cfg fileContents) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-contents.cataloger.enabled", true)
v.SetDefault("file-contents.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-contents.cataloger.scope", source.SquashedScope)
v.SetDefault("file-contents.skip-files-above-size", 1*file.MB)
v.SetDefault("file-contents.globs", []string{})
+1 -1
View File
@@ -11,7 +11,7 @@ type FileMetadata struct {
}
func (cfg FileMetadata) loadDefaultValues(v *viper.Viper) {
v.SetDefault("file-metadata.cataloger.enabled", true)
v.SetDefault("file-metadata.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("file-metadata.cataloger.scope", source.SquashedScope)
v.SetDefault("file-metadata.digests", []string{"sha256"})
}
+1 -1
View File
@@ -15,7 +15,7 @@ type secrets struct {
}
func (cfg secrets) loadDefaultValues(v *viper.Viper) {
v.SetDefault("secrets.cataloger.enabled", true)
v.SetDefault("secrets.cataloger.enabled", catalogerEnabledDefault)
v.SetDefault("secrets.cataloger.scope", source.AllLayersScope)
v.SetDefault("secrets.reveal-values", false)
v.SetDefault("secrets.skip-files-above-size", 1*file.MB)
+1 -1
View File
@@ -6,5 +6,5 @@ const (
// JSONSchemaVersion is the current schema version output by the JSON presenter
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "1.1.0"
JSONSchemaVersion = "2.0.0"
)
@@ -0,0 +1,19 @@
package spdxhelpers
import "github.com/anchore/syft/syft/pkg"
func Description(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
return metadata.Description
case pkg.NpmPackageJSONMetadata:
return metadata.Description
}
}
return ""
}
func hasMetadata(p pkg.Package) bool {
return p.Metadata != nil
}
@@ -0,0 +1,56 @@
package spdxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_Description(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "no metadata",
input: pkg.Package{},
expected: "",
},
{
name: "from apk",
input: pkg.Package{
Metadata: pkg.ApkMetadata{
Description: "a description!",
},
},
expected: "a description!",
},
{
name: "from npm",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Description: "a description!",
},
},
expected: "a description!",
},
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "empty",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Homepage: "",
},
},
expected: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Description(test.input))
})
}
}
@@ -0,0 +1,23 @@
package spdxhelpers
import "github.com/anchore/syft/syft/pkg"
func DownloadLocation(p pkg.Package) string {
// 3.7: Package Download Location
// Cardinality: mandatory, one
// NONE if there is no download location whatsoever.
// NOASSERTION if:
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
// (ii) the SPDX file creator has made no attempt to determine this field; or
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
return NoneIfEmpty(metadata.URL)
case pkg.NpmPackageJSONMetadata:
return NoneIfEmpty(metadata.URL)
}
}
return "NOASSERTION"
}
@@ -0,0 +1,54 @@
package spdxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_DownloadLocation(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
name: "no metadata",
input: pkg.Package{},
expected: "NOASSERTION",
},
{
name: "from apk",
input: pkg.Package{
Metadata: pkg.ApkMetadata{
URL: "http://a-place.gov",
},
},
expected: "http://a-place.gov",
},
{
name: "from npm",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
URL: "http://a-place.gov",
},
},
expected: "http://a-place.gov",
},
{
name: "empty",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
URL: "",
},
},
expected: "NONE",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, DownloadLocation(test.input))
})
}
}
@@ -0,0 +1,51 @@
package spdxhelpers
import (
"github.com/anchore/syft/internal/formats/spdx22json/model"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
)
func ExternalRefs(p pkg.Package) (externalRefs []model.ExternalRef) {
externalRefs = make([]model.ExternalRef, 0)
for _, c := range p.CPEs {
externalRefs = append(externalRefs, model.ExternalRef{
ReferenceCategory: model.SecurityReferenceCategory,
ReferenceLocator: c.BindToFmtString(),
ReferenceType: model.Cpe23ExternalRefType,
})
}
if p.PURL != "" {
externalRefs = append(externalRefs, model.ExternalRef{
ReferenceCategory: model.PackageManagerReferenceCategory,
ReferenceLocator: p.PURL,
ReferenceType: model.PurlExternalRefType,
})
}
return externalRefs
}
func ExtractPURL(refs []model.ExternalRef) string {
for _, r := range refs {
if r.ReferenceType == model.PurlExternalRefType {
return r.ReferenceLocator
}
}
return ""
}
func ExtractCPEs(refs []model.ExternalRef) (cpes []pkg.CPE) {
for _, r := range refs {
if r.ReferenceType == model.Cpe23ExternalRefType {
cpe, err := pkg.NewCPE(r.ReferenceLocator)
if err != nil {
log.Warnf("unable to extract SPDX CPE=%q: %+v", r.ReferenceLocator, err)
continue
}
cpes = append(cpes, cpe)
}
}
return cpes
}
@@ -0,0 +1,45 @@
package spdxhelpers
import (
"testing"
"github.com/anchore/syft/internal/formats/spdx22json/model"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_ExternalRefs(t *testing.T) {
testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
tests := []struct {
name string
input pkg.Package
expected []model.ExternalRef
}{
{
name: "cpe + purl",
input: pkg.Package{
CPEs: []pkg.CPE{
testCPE,
},
PURL: "a-purl",
},
expected: []model.ExternalRef{
{
ReferenceCategory: model.SecurityReferenceCategory,
ReferenceLocator: testCPE.BindToFmtString(),
ReferenceType: model.Cpe23ExternalRefType,
},
{
ReferenceCategory: model.PackageManagerReferenceCategory,
ReferenceLocator: "a-purl",
ReferenceType: model.PurlExternalRefType,
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, ExternalRefs(test.input))
})
}
}
@@ -0,0 +1,15 @@
package spdxhelpers
import "github.com/anchore/syft/syft/pkg"
func Homepage(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.GemMetadata:
return metadata.Homepage
case pkg.NpmPackageJSONMetadata:
return metadata.Homepage
}
}
return ""
}
@@ -0,0 +1,56 @@
package spdxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_Homepage(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "no metadata",
input: pkg.Package{},
expected: "",
},
{
name: "from gem",
input: pkg.Package{
Metadata: pkg.GemMetadata{
Homepage: "http://a-place.gov",
},
},
expected: "http://a-place.gov",
},
{
name: "from npm",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Homepage: "http://a-place.gov",
},
},
expected: "http://a-place.gov",
},
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "empty",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Homepage: "",
},
},
expected: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Homepage(test.input))
})
}
}
@@ -0,0 +1,37 @@
package spdxhelpers
import (
"strings"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/pkg"
)
func License(p pkg.Package) string {
// source: https://spdx.github.io/spdx-spec/3-package-information/#313-concluded-license
// The options to populate this field are limited to:
// A valid SPDX License Expression as defined in Appendix IV;
// NONE, if the SPDX file creator concludes there is no license available for this package; or
// NOASSERTION if:
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
// (ii) the SPDX file creator has made no attempt to determine this field; or
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
if len(p.Licenses) == 0 {
return "NONE"
}
// take all licenses and assume an AND expression; for information about license expressions see https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/
var parsedLicenses []string
for _, l := range p.Licenses {
if value, exists := spdxlicense.ID(l); exists {
parsedLicenses = append(parsedLicenses, value)
}
}
if len(parsedLicenses) == 0 {
return "NOASSERTION"
}
return strings.Join(parsedLicenses, " AND ")
}
@@ -0,0 +1,73 @@
package spdxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_License(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
name: "no licenses",
input: pkg.Package{},
expected: "NONE",
},
{
name: "no SPDX licenses",
input: pkg.Package{
Licenses: []string{
"made-up",
},
},
expected: "NOASSERTION",
},
{
name: "with SPDX license",
input: pkg.Package{
Licenses: []string{
"MIT",
},
},
expected: "MIT",
},
{
name: "with SPDX license expression",
input: pkg.Package{
Licenses: []string{
"MIT",
"GPL-3.0",
},
},
expected: "MIT AND GPL-3.0",
},
{
name: "cap insensitive",
input: pkg.Package{
Licenses: []string{
"gpl-3.0",
},
},
expected: "GPL-3.0",
},
{
name: "debian to spdx conversion",
input: pkg.Package{
Licenses: []string{
"GPL-2",
},
},
expected: "GPL-2.0",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, License(test.input))
})
}
}
@@ -0,0 +1,12 @@
package spdxhelpers
import (
"strings"
)
func NoneIfEmpty(value string) string {
if strings.TrimSpace(value) == "" {
return "NONE"
}
return value
}
@@ -0,0 +1,41 @@
package spdxhelpers
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_noneIfEmpty(t *testing.T) {
tests := []struct {
name string
value string
expected string
}{
{
name: "non-zero value",
value: "something",
expected: "something",
},
{
name: "empty",
value: "",
expected: "NONE",
},
{
name: "space",
value: " ",
expected: "NONE",
},
{
name: "tab",
value: "\t",
expected: "NONE",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, NoneIfEmpty(test.value))
})
}
}
@@ -0,0 +1,114 @@
package spdxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/stretchr/testify/assert"
)
func Test_Originator(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "no metadata",
input: pkg.Package{},
expected: "",
},
{
name: "from gem",
input: pkg.Package{
Metadata: pkg.GemMetadata{
Authors: []string{
"auth1",
"auth2",
},
},
},
expected: "auth1",
},
{
name: "from npm",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Author: "auth",
},
},
expected: "auth",
},
{
name: "from apk",
input: pkg.Package{
Metadata: pkg.ApkMetadata{
Maintainer: "auth",
},
},
expected: "auth",
},
{
name: "from python - just name",
input: pkg.Package{
Metadata: pkg.PythonPackageMetadata{
Author: "auth",
},
},
expected: "auth",
},
{
name: "from python - just email",
input: pkg.Package{
Metadata: pkg.PythonPackageMetadata{
AuthorEmail: "auth@auth.gov",
},
},
expected: "auth@auth.gov",
},
{
name: "from python - both name and email",
input: pkg.Package{
Metadata: pkg.PythonPackageMetadata{
Author: "auth",
AuthorEmail: "auth@auth.gov",
},
},
expected: "auth <auth@auth.gov>",
},
{
name: "from rpm",
input: pkg.Package{
Metadata: pkg.RpmdbMetadata{
Vendor: "auth",
},
},
expected: "auth",
},
{
name: "from dpkg",
input: pkg.Package{
Metadata: pkg.DpkgMetadata{
Maintainer: "auth",
},
},
expected: "auth",
},
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "empty",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Author: "",
},
},
expected: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, Originator(test.input))
})
}
}
@@ -0,0 +1,37 @@
package spdxhelpers
import (
"fmt"
"github.com/anchore/syft/syft/pkg"
)
func Originator(p pkg.Package) string {
if hasMetadata(p) {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
return metadata.Maintainer
case pkg.NpmPackageJSONMetadata:
return metadata.Author
case pkg.PythonPackageMetadata:
author := metadata.Author
if author == "" {
return metadata.AuthorEmail
}
if metadata.AuthorEmail != "" {
author += fmt.Sprintf(" <%s>", metadata.AuthorEmail)
}
return author
case pkg.GemMetadata:
if len(metadata.Authors) > 0 {
return metadata.Authors[0]
}
return ""
case pkg.RpmdbMetadata:
return metadata.Vendor
case pkg.DpkgMetadata:
return metadata.Maintainer
}
}
return ""
}
@@ -0,0 +1,41 @@
package spdxhelpers
import (
"strings"
"github.com/anchore/syft/syft/pkg"
)
func SourceInfo(p pkg.Package) string {
answer := ""
switch p.Type {
case pkg.RpmPkg:
answer = "acquired package info from RPM DB"
case pkg.ApkPkg:
answer = "acquired package info from APK DB"
case pkg.DebPkg:
answer = "acquired package info from DPKG DB"
case pkg.NpmPkg:
answer = "acquired package info from installed node module manifest file"
case pkg.PythonPkg:
answer = "acquired package info from installed python package manifest file"
case pkg.JavaPkg, pkg.JenkinsPluginPkg:
answer = "acquired package info from installed java archive"
case pkg.GemPkg:
answer = "acquired package info from installed gem metadata file"
case pkg.GoModulePkg:
answer = "acquired package info from go module information"
case pkg.RustPkg:
answer = "acquired package info from rust cargo manifest"
case pkg.PhpComposerPkg:
answer = "acquired package info from PHP composer manifest"
default:
answer = "acquired package info from the following paths"
}
var paths []string
for _, l := range p.Locations {
paths = append(paths, l.RealPath)
}
return answer + ": " + strings.Join(paths, ", ")
}
@@ -0,0 +1,143 @@
package spdxhelpers
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/assert"
)
func Test_SourceInfo(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected []string
}{
{
name: "locations are captured",
input: pkg.Package{
// note: no type given
Locations: []source.Location{
source.NewVirtualLocation("/a-place", "/b-place"),
source.NewVirtualLocation("/c-place", "/d-place"),
},
},
expected: []string{
"from the following paths",
"/a-place",
"/c-place",
},
},
{
// note: no specific support for this
input: pkg.Package{
Type: pkg.KbPkg,
},
expected: []string{
"from the following paths",
},
},
{
input: pkg.Package{
Type: pkg.RpmPkg,
},
expected: []string{
"from RPM DB",
},
},
{
input: pkg.Package{
Type: pkg.ApkPkg,
},
expected: []string{
"from APK DB",
},
},
{
input: pkg.Package{
Type: pkg.DebPkg,
},
expected: []string{
"from DPKG DB",
},
},
{
input: pkg.Package{
Type: pkg.NpmPkg,
},
expected: []string{
"from installed node module manifest file",
},
},
{
input: pkg.Package{
Type: pkg.PythonPkg,
},
expected: []string{
"from installed python package manifest file",
},
},
{
input: pkg.Package{
Type: pkg.JavaPkg,
},
expected: []string{
"from installed java archive",
},
},
{
input: pkg.Package{
Type: pkg.JenkinsPluginPkg,
},
expected: []string{
"from installed java archive",
},
},
{
input: pkg.Package{
Type: pkg.GemPkg,
},
expected: []string{
"from installed gem metadata file",
},
},
{
input: pkg.Package{
Type: pkg.GoModulePkg,
},
expected: []string{
"from go module information",
},
},
{
input: pkg.Package{
Type: pkg.RustPkg,
},
expected: []string{
"from rust cargo manifest",
},
},
{
input: pkg.Package{
Type: pkg.PhpComposerPkg,
},
expected: []string{
"from PHP composer manifest",
},
},
}
var pkgTypes []pkg.Type
for _, test := range tests {
t.Run(test.name+" "+string(test.input.Type), func(t *testing.T) {
if test.input.Type != "" {
pkgTypes = append(pkgTypes, test.input.Type)
}
actual := SourceInfo(test.input)
for _, expected := range test.expected {
assert.Contains(t, actual, expected)
}
})
}
assert.ElementsMatch(t, pkg.AllPkgs, pkgTypes, "missing one or more package types to test against (maybe a package type was added?)")
}
@@ -1,15 +1,17 @@
package packages
package testutils
import (
"bytes"
"testing"
"github.com/anchore/go-presenter"
"github.com/anchore/go-testutils"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/stretchr/testify/assert"
@@ -17,7 +19,19 @@ import (
type redactor func(s []byte) []byte
func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
type imageCfg struct {
fromSnapshot bool
}
type ImageOption func(cfg *imageCfg)
func FromSnapshot() ImageOption {
return func(cfg *imageCfg) {
cfg.fromSnapshot = true
}
}
func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
var buffer bytes.Buffer
// grab the latest image contents and persist
@@ -50,7 +64,7 @@ func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
}
}
func assertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
var buffer bytes.Buffer
err := pres.Present(&buffer)
@@ -77,17 +91,57 @@ func assertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter
}
}
func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, *distro.Distro) {
func ImageInput(t testing.TB, testImage string, options ...ImageOption) sbom.SBOM {
t.Helper()
catalog := pkg.NewCatalog()
img := imagetest.GetGoldenFixtureImage(t, testImage)
var cfg imageCfg
var img *image.Image
for _, opt := range options {
opt(&cfg)
}
switch cfg.fromSnapshot {
case true:
img = imagetest.GetGoldenFixtureImage(t, testImage)
default:
img = imagetest.GetFixtureImage(t, "docker-archive", testImage)
}
populateImageCatalog(catalog, img)
// this is a hard coded value that is not given by the fixture helper and must be provided manually
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
src, err := source.NewFromImage(img, "user-image-input")
assert.NoError(t, err)
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
assert.NoError(t, err)
return sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: catalog,
Distro: &dist,
},
Source: src.Metadata,
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "v0.42.0-bogus",
// the application configuration should be persisted here, however, we do not want to import
// the application configuration in this package (it's reserved only for ingestion by the cmd package)
Configuration: map[string]string{
"config-key": "config-value",
},
},
}
}
func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
// populate catalog with test data
catalog.Add(pkg.Package{
ID: "package-1-id",
Name: "package-1",
Version: "1.0.1",
Locations: []source.Location{
@@ -104,11 +158,10 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M
},
PURL: "a-purl-1",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
pkg.MustCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
},
})
catalog.Add(pkg.Package{
ID: "package-2-id",
Name: "package-2",
Version: "2.0.1",
Locations: []source.Location{
@@ -123,34 +176,49 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
},
})
}
// this is a hard coded value that is not given by the fixture helper and must be provided manually
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
src, err := source.NewFromImage(img, "user-image-input")
assert.NoError(t, err)
func DirectoryInput(t testing.TB) sbom.SBOM {
catalog := newDirectoryCatalog()
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
assert.NoError(t, err)
return catalog, src.Metadata, &dist
src, err := source.NewFromDirectory("/some/path")
assert.NoError(t, err)
return sbom.SBOM{
Artifacts: sbom.Artifacts{
PackageCatalog: catalog,
Distro: &dist,
},
Source: src.Metadata,
Descriptor: sbom.Descriptor{
Name: "syft",
Version: "v0.42.0-bogus",
// the application configuration should be persisted here, however, we do not want to import
// the application configuration in this package (it's reserved only for ingestion by the cmd package)
Configuration: map[string]string{
"config-key": "config-value",
},
},
}
}
func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) {
func newDirectoryCatalog() *pkg.Catalog {
catalog := pkg.NewCatalog()
// populate catalog with test data
catalog.Add(pkg.Package{
ID: "package-1-id",
Name: "package-1",
Version: "1.0.1",
Type: pkg.PythonPkg,
FoundBy: "the-cataloger-1",
Locations: []source.Location{
{RealPath: "/some/path/pkg1"},
source.NewLocation("/some/path/pkg1"),
},
Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType,
@@ -160,23 +228,22 @@ func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *dist
Version: "1.0.1",
Files: []pkg.PythonFileRecord{
{
Path: "/some/path/pkg1/depedencies/foo",
Path: "/some/path/pkg1/dependencies/foo",
},
},
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
},
})
catalog.Add(pkg.Package{
ID: "package-2-id",
Name: "package-2",
Version: "2.0.1",
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
Locations: []source.Location{
{RealPath: "/some/path/pkg1"},
source.NewLocation("/some/path/pkg1"),
},
MetadataType: pkg.DpkgMetadataType,
Metadata: pkg.DpkgMetadata{
@@ -185,15 +252,9 @@ func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *dist
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
},
})
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
assert.NoError(t, err)
src, err := source.NewFromDirectory("/some/path")
assert.NoError(t, err)
return catalog, src.Metadata, &dist
return catalog
}
@@ -0,0 +1,26 @@
package cyclonedx12xml
import (
"encoding/xml"
"io"
"github.com/anchore/syft/syft/sbom"
)
func encoder(output io.Writer, s sbom.SBOM) error {
enc := xml.NewEncoder(output)
enc.Indent("", " ")
_, err := output.Write([]byte(xml.Header))
if err != nil {
return err
}
err = enc.Encode(toFormatModel(s))
if err != nil {
return err
}
_, err = output.Write([]byte("\n"))
return err
}
@@ -1,17 +1,18 @@
package packages
package cyclonedx12xml
import (
"flag"
"regexp"
"testing"
"github.com/anchore/syft/internal/formats/common/testutils"
)
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
func TestCycloneDxDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
NewCycloneDxPresenter(catalog, metadata),
testutils.AssertPresenterAgainstGoldenSnapshot(t,
Format().Presenter(testutils.DirectoryInput(t)),
*updateCycloneDx,
cycloneDxRedactor,
)
@@ -19,9 +20,8 @@ func TestCycloneDxDirectoryPresenter(t *testing.T) {
func TestCycloneDxImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
NewCycloneDxPresenter(catalog, metadata),
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
Format().Presenter(testutils.ImageInput(t, testImage)),
testImage,
*updateCycloneDx,
cycloneDxRedactor,
+12
View File
@@ -0,0 +1,12 @@
package cyclonedx12xml
import "github.com/anchore/syft/syft/format"
func Format() format.Format {
return format.NewFormat(
format.CycloneDxOption,
encoder,
nil,
nil,
)
}
@@ -0,0 +1,31 @@
package model
import (
"encoding/xml"
)
// Source: https://cyclonedx.org/ext/bom-descriptor/
// BomDescriptor represents all metadata surrounding the BOM report (such as when the BOM was made, with which tool, and the item being cataloged).
type BomDescriptor struct {
XMLName xml.Name `xml:"metadata"`
Timestamp string `xml:"timestamp,omitempty"` // The date and time (timestamp) when the document was created
Tools []BomDescriptorTool `xml:"tools>tool"` // The tool used to create the BOM.
Component *BomDescriptorComponent `xml:"component"` // The component that the BOM describes.
}
// BomDescriptorTool represents the tool that created the BOM report.
type BomDescriptorTool struct {
XMLName xml.Name `xml:"tool"`
Vendor string `xml:"vendor,omitempty"` // The vendor of the tool used to create the BOM.
Name string `xml:"name,omitempty"` // The name of the tool used to create the BOM.
Version string `xml:"version,omitempty"` // The version of the tool used to create the BOM.
// TODO: hashes, author, manufacture, supplier
// TODO: add user-defined fields for the remaining build/version parameters
}
// BomDescriptorComponent represents the software/package being cataloged.
type BomDescriptorComponent struct {
XMLName xml.Name `xml:"component"`
Component
}
@@ -0,0 +1,20 @@
package model
import "encoding/xml"
// Component represents a single element in the CycloneDX BOM
type Component struct {
XMLName xml.Name `xml:"component"`
Type string `xml:"type,attr"` // Required; Describes if the component is a library, framework, application, container, operating system, firmware, hardware device, or file
Supplier string `xml:"supplier,omitempty"` // The organization that supplied the component. The supplier may often be the manufacture, but may also be a distributor or repackager.
Author string `xml:"author,omitempty"` // The person(s) or organization(s) that authored the component
Publisher string `xml:"publisher,omitempty"` // The person(s) or organization(s) that published the component
Group string `xml:"group,omitempty"` // The high-level classification that a project self-describes as. This will often be a shortened, single name of the company or project that produced the component, or the source package or domain name.
Name string `xml:"name"` // Required; The name of the component as defined by the project
Version string `xml:"version"` // Required; The version of the component as defined by the project
Description string `xml:"description,omitempty"` // A description of the component
Licenses *[]License `xml:"licenses>license"` // A node describing zero or more license names, SPDX license IDs or expressions
PackageURL string `xml:"purl,omitempty"` // Specifies the package-url (PackageURL). The purl, if specified, must be valid and conform to the specification defined at: https://github.com/package-url/purl-spec
// TODO: source, hashes, copyright, cpe, purl, swid, modified, pedigree, externalReferences
// TODO: add user-defined parameters for syft-specific values (image layer index, cataloger, location path, etc.)
}
@@ -0,0 +1,17 @@
package model
import (
"encoding/xml"
)
// Source: https://github.com/CycloneDX/specification
// Document represents a CycloneDX BOM Document.
type Document struct {
XMLName xml.Name `xml:"bom"`
XMLNs string `xml:"xmlns,attr"`
Version int `xml:"version,attr"`
SerialNumber string `xml:"serialNumber,attr"`
BomDescriptor *BomDescriptor `xml:"metadata"` // The BOM descriptor extension
Components []Component `xml:"components>component"` // The BOM contents
}
@@ -0,0 +1,10 @@
package model
import "encoding/xml"
// License represents a single software license for a Component
type License struct {
XMLName xml.Name `xml:"license"`
ID string `xml:"id,omitempty"` // A valid SPDX license ID
Name string `xml:"name,omitempty"` // If SPDX does not define the license used, this field may be used to provide the license name
}
@@ -0,0 +1,95 @@
package cyclonedx12xml
import (
"encoding/xml"
"time"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/formats/cyclonedx12xml/model"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/google/uuid"
)
// toFormatModel creates and populates a new in-memory representation of a CycloneDX 1.2 document
func toFormatModel(s sbom.SBOM) model.Document {
versionInfo := version.FromBuild()
doc := model.Document{
XMLNs: "http://cyclonedx.org/schema/bom/1.2",
Version: 1,
SerialNumber: uuid.New().URN(),
BomDescriptor: toBomDescriptor(internal.ApplicationName, versionInfo.Version, s.Source),
}
// attach components
for _, p := range s.Artifacts.PackageCatalog.Sorted() {
doc.Components = append(doc.Components, toComponent(p))
}
return doc
}
func toComponent(p pkg.Package) model.Component {
return model.Component{
Type: "library", // TODO: this is not accurate
Name: p.Name,
Version: p.Version,
PackageURL: p.PURL,
Licenses: toLicenses(p.Licenses),
}
}
// NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details.
func toBomDescriptor(name, version string, srcMetadata source.Metadata) *model.BomDescriptor {
return &model.BomDescriptor{
XMLName: xml.Name{},
Timestamp: time.Now().Format(time.RFC3339),
Tools: []model.BomDescriptorTool{
{
Vendor: "anchore",
Name: name,
Version: version,
},
},
Component: toBomDescriptorComponent(srcMetadata),
}
}
func toBomDescriptorComponent(srcMetadata source.Metadata) *model.BomDescriptorComponent {
switch srcMetadata.Scheme {
case source.ImageScheme:
return &model.BomDescriptorComponent{
Component: model.Component{
Type: "container",
Name: srcMetadata.ImageMetadata.UserInput,
Version: srcMetadata.ImageMetadata.ManifestDigest,
},
}
case source.DirectoryScheme:
return &model.BomDescriptorComponent{
Component: model.Component{
Type: "file",
Name: srcMetadata.Path,
},
}
}
return nil
}
func toLicenses(licenses []string) *[]model.License {
if len(licenses) == 0 {
return nil
}
var result []model.License
for _, licenseName := range licenses {
result = append(result, model.License{
Name: licenseName,
})
}
return &result
}
+44
View File
@@ -0,0 +1,44 @@
package formats
import (
"bytes"
"github.com/anchore/syft/internal/formats/cyclonedx12xml"
"github.com/anchore/syft/internal/formats/spdx22json"
"github.com/anchore/syft/internal/formats/spdx22tagvalue"
"github.com/anchore/syft/internal/formats/syftjson"
"github.com/anchore/syft/internal/formats/table"
"github.com/anchore/syft/internal/formats/text"
"github.com/anchore/syft/syft/format"
)
// TODO: eventually this is the source of truth for all formatters
func All() []format.Format {
return []format.Format{
syftjson.Format(),
table.Format(),
cyclonedx12xml.Format(),
spdx22json.Format(),
spdx22tagvalue.Format(),
text.Format(),
}
}
func Identify(by []byte) (*format.Format, error) {
for _, f := range All() {
if err := f.Validate(bytes.NewReader(by)); err != nil {
continue
}
return &f, nil
}
return nil, nil
}
func ByOption(option format.Option) *format.Format {
for _, f := range All() {
if f.Option == option {
return &f
}
}
return nil
}
+34
View File
@@ -0,0 +1,34 @@
package formats
import (
"io"
"os"
"testing"
"github.com/anchore/syft/syft/format"
"github.com/stretchr/testify/assert"
)
func TestIdentify(t *testing.T) {
tests := []struct {
fixture string
expected format.Option
}{
{
fixture: "test-fixtures/alpine-syft.json",
expected: format.JSONOption,
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
f, err := os.Open(test.fixture)
assert.NoError(t, err)
by, err := io.ReadAll(f)
assert.NoError(t, err)
frmt, err := Identify(by)
assert.NoError(t, err)
assert.NotNil(t, frmt)
assert.Equal(t, test.expected, frmt.Option)
})
}
}
+21
View File
@@ -0,0 +1,21 @@
package spdx22json
import (
"encoding/json"
"io"
"github.com/anchore/syft/syft/sbom"
)
const anchoreNamespace = "https://anchore.com/syft"
func encoder(output io.Writer, s sbom.SBOM) error {
doc := toFormatModel(s)
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(&doc)
}
@@ -1,17 +1,18 @@
package packages
package spdx22json
import (
"flag"
"regexp"
"testing"
"github.com/anchore/syft/internal/formats/common/testutils"
)
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json presenters")
func TestSPDXJSONDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
NewSPDXJSONPresenter(catalog, metadata),
testutils.AssertPresenterAgainstGoldenSnapshot(t,
Format().Presenter(testutils.DirectoryInput(t)),
*updateSpdxJson,
spdxJsonRedactor,
)
@@ -19,9 +20,8 @@ func TestSPDXJSONDirectoryPresenter(t *testing.T) {
func TestSPDXJSONImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
NewSPDXJSONPresenter(catalog, metadata),
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
testImage,
*updateSpdxJson,
spdxJsonRedactor,
+13
View File
@@ -0,0 +1,13 @@
package spdx22json
import "github.com/anchore/syft/syft/format"
// note: this format is LOSSY relative to the syftjson formation, which means that decoding and validation is not supported at this time
func Format() format.Format {
return format.NewFormat(
format.SPDXJSONOption,
encoder,
nil,
nil,
)
}
@@ -1,4 +1,4 @@
package spdx22
package model
import "time"
@@ -1,4 +1,4 @@
package spdx22
package model
type Checksum struct {
// Identifies the algorithm used to produce the subject Checksum. One of: "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224"
@@ -1,4 +1,4 @@
package spdx22
package model
import "time"
@@ -10,7 +10,7 @@ type CreationInfo struct {
Created time.Time `json:"created"`
// Identify who (or what, in the case of a tool) created the SPDX file. If the SPDX file was created by an
// individual, indicate the person's name. If the SPDX file was created on behalf of a company or organization,
//indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version
// indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version
// for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person
// name or organization name may be designated as “anonymous” if appropriate.
Creators []string `json:"creators"`
@@ -1,4 +1,4 @@
package spdx22
package model
// derived from:
// - https://spdx.github.io/spdx-spec/appendix-III-RDF-data-model-implementation-and-identifier-syntax/
@@ -1,4 +1,4 @@
package spdx22
package model
type Element struct {
SPDXID string `json:"SPDXID"`
@@ -1,4 +1,4 @@
package spdx22
package model
// ElementID represents the identifier string portion of an SPDX element
// identifier. DocElementID should be used for any attributes which can
@@ -1,4 +1,4 @@
package spdx22
package model
type ExternalDocumentRef struct {
// externalDocumentId is a string containing letters, numbers, ., - and/or + which uniquely identifies an external document within this document.
@@ -1,4 +1,4 @@
package spdx22
package model
type ReferenceCategory string
@@ -1,19 +1,19 @@
package spdx22
package model
type FileType string
const (
DocumentationFileType FileType = "DOCUMENTATION"
ImageFileType FileType = "IMAGE"
VideoFileType FileType = "VIDEO"
ArchiveFileType FileType = "ARCHIVE"
SpdxFileType FileType = "SPDX"
ApplicationFileType FileType = "APPLICATION"
SourceFileType FileType = "SOURCE"
BinaryFileType FileType = "BINARY"
TextFileType FileType = "TEXT"
AudioFileType FileType = "AUDIO"
OtherFileType FileType = "OTHER"
DocumentationFileType FileType = "DOCUMENTATION" // if the file serves as documentation
ImageFileType FileType = "IMAGE" // if the file is associated with a picture image file (MIME type of image/*, e.g., .jpg, .gif)
VideoFileType FileType = "VIDEO" // if the file is associated with a video file type (MIME type of video/*)
ArchiveFileType FileType = "ARCHIVE" // if the file represents an archive (.tar, .jar, etc.)
SpdxFileType FileType = "SPDX" // if the file is an SPDX document
ApplicationFileType FileType = "APPLICATION" // if the file is associated with a specific application type (MIME type of application/*)
SourceFileType FileType = "SOURCE" // if the file is human readable source code (.c, .html, etc.)
BinaryFileType FileType = "BINARY" // if the file is a compiled object, target image or binary executable (.o, .a, etc.)
TextFileType FileType = "TEXT" // if the file is human readable text file (MIME type of text/*)
AudioFileType FileType = "AUDIO" // if the file is associated with an audio file (MIME type of audio/* , e.g. .mp3)
OtherFileType FileType = "OTHER" // if the file doesn't fit into the above categories (generated artifacts, data files, etc.)
)
type File struct {
@@ -36,6 +36,6 @@ type File struct {
// Indicates the project in which the SpdxElement originated. Tools must preserve doap:homepage and doap:name
// properties and the URI (if one is known) of doap:Project resources that are values of this property. All other
// properties of doap:Projects are not directly supported by SPDX and may be dropped when translating to or
// from some SPDX formats(deprecated).
// from some SPDX formats (deprecated).
ArtifactOf []string `json:"artifactOf,omitempty"`
}
@@ -1,4 +1,4 @@
package spdx22
package model
type HasExtractedLicensingInfo struct {
// Verbatim license or licensing notice text that was discovered.
@@ -1,4 +1,4 @@
package spdx22
package model
type Item struct {
Element
@@ -1,4 +1,4 @@
package spdx22
package model
type Package struct {
Item
@@ -1,4 +1,4 @@
package spdx22
package model
// Why are there two package identifier fields Package Checksum and Package Verification?
// Although the values of the two fields Package Checksum and Package Verification are similar, they each serve a
@@ -1,4 +1,4 @@
package spdx22
package model
type Relationship struct {
// Id to which the SPDX element is related
@@ -1,4 +1,4 @@
package spdx22
package model
type StartPointer struct {
Offset int `json:"offset,omitempty"`
@@ -1,3 +1,3 @@
package spdx22
package model
const Version = "SPDX-2.2"
@@ -0,0 +1,4 @@
# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.
FROM scratch
ADD file-1.txt /somefile-1.txt
ADD file-2.txt /somefile-2.txt
@@ -0,0 +1 @@
this file has contents
@@ -0,0 +1 @@
file-2 contents!
@@ -3,18 +3,18 @@
"name": "/some/path",
"spdxVersion": "SPDX-2.2",
"creationInfo": {
"created": "2021-09-16T20:44:35.198887Z",
"created": "2021-11-17T19:35:54.834877Z",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-[not provided]"
],
"licenseListVersion": "3.14"
"licenseListVersion": "3.15"
},
"dataLicense": "CC0-1.0",
"documentNamespace": "https://anchore.com/syft/image/",
"documentNamespace": "https:/anchore.com/syft/dir/some/path-65e2226e-a61e-4ed1-81bb-56022e1ff1eb",
"packages": [
{
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
"SPDXID": "SPDXRef-2a115ac97d018a0e",
"name": "package-1",
"licenseConcluded": "MIT",
"downloadLocation": "NOASSERTION",
@@ -31,15 +31,12 @@
}
],
"filesAnalyzed": false,
"hasFiles": [
"SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9"
],
"licenseDeclared": "MIT",
"sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1",
"versionInfo": "1.0.1"
},
{
"SPDXID": "SPDXRef-Package-deb-package-2-2.0.1",
"SPDXID": "SPDXRef-5e920b2bece2c3ae",
"name": "package-2",
"licenseConcluded": "NONE",
"downloadLocation": "NOASSERTION",
@@ -60,20 +57,5 @@
"sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1",
"versionInfo": "2.0.1"
}
],
"files": [
{
"SPDXID": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9",
"name": "foo",
"licenseConcluded": "",
"fileName": "/some/path/pkg1/depedencies/foo"
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-Package-python-package-1-1.0.1",
"relationshipType": "CONTAINS",
"relatedSpdxElement": "SPDXRef-File-package-1-04cd22424378dcd6c77fce08beb52493b5494a60ea5e1f9bdf9b16dc0cacffe9"
}
]
}
@@ -3,18 +3,18 @@
"name": "user-image-input",
"spdxVersion": "SPDX-2.2",
"creationInfo": {
"created": "2021-09-16T20:44:35.203911Z",
"created": "2021-11-17T19:35:57.761372Z",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-[not provided]"
],
"licenseListVersion": "3.14"
"licenseListVersion": "3.15"
},
"dataLicense": "CC0-1.0",
"documentNamespace": "https://anchore.com/syft/image/user-image-input",
"documentNamespace": "https:/anchore.com/syft/image/user-image-input-5383918f-ec96-4aa9-b756-ad16e1ada31e",
"packages": [
{
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
"SPDXID": "SPDXRef-888661d4f0362f02",
"name": "package-1",
"licenseConcluded": "MIT",
"downloadLocation": "NOASSERTION",
@@ -36,7 +36,7 @@
"versionInfo": "1.0.1"
},
{
"SPDXID": "SPDXRef-Package-deb-package-2-2.0.1",
"SPDXID": "SPDXRef-4068ff5e8926b305",
"name": "package-2",
"licenseConcluded": "NONE",
"downloadLocation": "NOASSERTION",
@@ -0,0 +1,266 @@
package spdx22json
import (
"fmt"
"path"
"path/filepath"
"sort"
"strings"
"time"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/formats/common/spdxhelpers"
"github.com/anchore/syft/internal/formats/spdx22json/model"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
"github.com/google/uuid"
)
// toFormatModel creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results.
func toFormatModel(s sbom.SBOM) model.Document {
name := documentName(s.Source)
return model.Document{
Element: model.Element{
SPDXID: model.ElementID("DOCUMENT").String(),
Name: name,
},
SPDXVersion: model.Version,
CreationInfo: model.CreationInfo{
Created: time.Now().UTC(),
Creators: []string{
// note: key-value format derived from the JSON example document examples: https://github.com/spdx/spdx-spec/blob/v2.2/examples/SPDXJSONExample-v2.2.spdx.json
"Organization: Anchore, Inc",
"Tool: " + internal.ApplicationName + "-" + version.FromBuild().Version,
},
LicenseListVersion: spdxlicense.Version,
},
DataLicense: "CC0-1.0",
DocumentNamespace: documentNamespace(name, s.Source),
Packages: toPackages(s.Artifacts.PackageCatalog, s.Relationships),
Files: toFiles(s),
Relationships: toRelationships(s.Relationships),
}
}
func documentName(srcMetadata source.Metadata) string {
switch srcMetadata.Scheme {
case source.ImageScheme:
return cleanSPDXName(srcMetadata.ImageMetadata.UserInput)
case source.DirectoryScheme:
return cleanSPDXName(srcMetadata.Path)
}
// TODO: is this alright?
return uuid.Must(uuid.NewRandom()).String()
}
func cleanSPDXName(name string) string {
// remove # according to specification
name = strings.ReplaceAll(name, "#", "-")
// remove : for url construction
name = strings.ReplaceAll(name, ":", "-")
// clean relative pathing
return path.Clean(name)
}
func documentNamespace(name string, srcMetadata source.Metadata) string {
input := "unknown-source-type"
switch srcMetadata.Scheme {
case source.ImageScheme:
input = "image"
case source.DirectoryScheme:
input = "dir"
}
uniqueID := uuid.Must(uuid.NewRandom())
identifier := path.Join(input, uniqueID.String())
if name != "." {
identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String()))
}
return path.Join(anchoreNamespace, identifier)
}
func toPackages(catalog *pkg.Catalog, relationships []artifact.Relationship) []model.Package {
packages := make([]model.Package, 0)
for _, p := range catalog.Sorted() {
license := spdxhelpers.License(p)
packageSpdxID := model.ElementID(p.ID()).String()
// note: the license concluded and declared should be the same since we are collecting license information
// from the project data itself (the installed package files).
packages = append(packages, model.Package{
Description: spdxhelpers.Description(p),
DownloadLocation: spdxhelpers.DownloadLocation(p),
ExternalRefs: spdxhelpers.ExternalRefs(p),
FilesAnalyzed: false,
HasFiles: fileIDsForPackage(packageSpdxID, relationships),
Homepage: spdxhelpers.Homepage(p),
// The Declared License is what the authors of a project believe govern the package
LicenseDeclared: license,
Originator: spdxhelpers.Originator(p),
SourceInfo: spdxhelpers.SourceInfo(p),
VersionInfo: p.Version,
Item: model.Item{
// The Concluded License field is the license the SPDX file creator believes governs the package
LicenseConcluded: license,
Element: model.Element{
SPDXID: packageSpdxID,
Name: p.Name,
},
},
})
}
return packages
}
func fileIDsForPackage(packageSpdxID string, relationships []artifact.Relationship) (fileIDs []string) {
for _, relationship := range relationships {
if relationship.Type != artifact.ContainsRelationship {
continue
}
if _, ok := relationship.From.(pkg.Package); !ok {
continue
}
if _, ok := relationship.To.(source.Coordinates); !ok {
continue
}
if string(relationship.From.ID()) == packageSpdxID {
fileIDs = append(fileIDs, string(relationship.To.ID()))
}
}
return fileIDs
}
func toFiles(s sbom.SBOM) []model.File {
results := make([]model.File, 0)
artifacts := s.Artifacts
for _, coordinates := range sbom.AllCoordinates(s) {
var metadata *source.FileMetadata
if metadataForLocation, exists := artifacts.FileMetadata[coordinates]; exists {
metadata = &metadataForLocation
}
var digests []file.Digest
if digestsForLocation, exists := artifacts.FileDigests[coordinates]; exists {
digests = digestsForLocation
}
// TODO: add file classifications (?) and content as a snippet
var comment string
if coordinates.FileSystemID != "" {
comment = fmt.Sprintf("layerID: %s", coordinates.FileSystemID)
}
results = append(results, model.File{
Item: model.Item{
Element: model.Element{
SPDXID: string(coordinates.ID()),
Name: filepath.Base(coordinates.RealPath),
Comment: comment,
},
// required, no attempt made to determine license information
LicenseConcluded: "NOASSERTION",
},
Checksums: toFileChecksums(digests),
FileName: coordinates.RealPath,
FileTypes: toFileTypes(metadata),
})
}
// sort by real path then virtual path to ensure the result is stable across multiple runs
sort.SliceStable(results, func(i, j int) bool {
return results[i].FileName < results[j].FileName
})
return results
}
func toFileChecksums(digests []file.Digest) (checksums []model.Checksum) {
for _, digest := range digests {
checksums = append(checksums, model.Checksum{
Algorithm: digest.Algorithm,
ChecksumValue: digest.Value,
})
}
return checksums
}
func toFileTypes(metadata *source.FileMetadata) (ty []string) {
if metadata == nil {
return nil
}
mimeTypePrefix := strings.Split(metadata.MIMEType, "/")[0]
switch mimeTypePrefix {
case "image":
ty = append(ty, string(model.ImageFileType))
case "video":
ty = append(ty, string(model.VideoFileType))
case "application":
ty = append(ty, string(model.ApplicationFileType))
case "text":
ty = append(ty, string(model.TextFileType))
case "audio":
ty = append(ty, string(model.AudioFileType))
}
if internal.IsExecutable(metadata.MIMEType) {
ty = append(ty, string(model.BinaryFileType))
}
if internal.IsArchive(metadata.MIMEType) {
ty = append(ty, string(model.ArchiveFileType))
}
// TODO: add support for source, spdx, and documentation file types
if len(ty) == 0 {
ty = append(ty, string(model.OtherFileType))
}
return ty
}
func toRelationships(relationships []artifact.Relationship) (result []model.Relationship) {
for _, r := range relationships {
exists, relationshipType, comment := lookupRelationship(r.Type)
if !exists {
log.Warnf("unable to convert relationship from SPDX 2.2 JSON, dropping: %+v", r)
continue
}
result = append(result, model.Relationship{
SpdxElementID: string(r.From.ID()),
RelationshipType: relationshipType,
RelatedSpdxElement: string(r.To.ID()),
Comment: comment,
})
}
return result
}
func lookupRelationship(ty artifact.RelationshipType) (bool, model.RelationshipType, string) {
switch ty {
case artifact.ContainsRelationship:
return true, model.ContainsRelationship, ""
case artifact.OwnershipByFileOverlapRelationship:
return true, model.OtherRelationship, fmt.Sprintf("%s: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by", ty)
}
return false, "", ""
}
@@ -0,0 +1,256 @@
package spdx22json
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/internal/formats/spdx22json/model"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/assert"
)
func Test_toFileTypes(t *testing.T) {
tests := []struct {
name string
metadata source.FileMetadata
expected []string
}{
{
name: "application",
metadata: source.FileMetadata{
MIMEType: "application/vnd.unknown",
},
expected: []string{
string(model.ApplicationFileType),
},
},
{
name: "archive",
metadata: source.FileMetadata{
MIMEType: "application/zip",
},
expected: []string{
string(model.ApplicationFileType),
string(model.ArchiveFileType),
},
},
{
name: "audio",
metadata: source.FileMetadata{
MIMEType: "audio/ogg",
},
expected: []string{
string(model.AudioFileType),
},
},
{
name: "video",
metadata: source.FileMetadata{
MIMEType: "video/3gpp",
},
expected: []string{
string(model.VideoFileType),
},
},
{
name: "text",
metadata: source.FileMetadata{
MIMEType: "text/html",
},
expected: []string{
string(model.TextFileType),
},
},
{
name: "image",
metadata: source.FileMetadata{
MIMEType: "image/png",
},
expected: []string{
string(model.ImageFileType),
},
},
{
name: "binary",
metadata: source.FileMetadata{
MIMEType: "application/x-sharedlib",
},
expected: []string{
string(model.ApplicationFileType),
string(model.BinaryFileType),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, toFileTypes(&test.metadata))
})
}
}
func Test_lookupRelationship(t *testing.T) {
tests := []struct {
input artifact.RelationshipType
exists bool
ty model.RelationshipType
comment string
}{
{
input: artifact.ContainsRelationship,
exists: true,
ty: model.ContainsRelationship,
},
{
input: artifact.OwnershipByFileOverlapRelationship,
exists: true,
ty: model.OtherRelationship,
comment: "ownership-by-file-overlap: indicates that the parent package claims ownership of a child package since the parent metadata indicates overlap with a location that a cataloger found the child package by",
},
{
input: "made-up",
exists: false,
},
}
for _, test := range tests {
t.Run(string(test.input), func(t *testing.T) {
exists, ty, comment := lookupRelationship(test.input)
assert.Equal(t, exists, test.exists)
assert.Equal(t, ty, test.ty)
assert.Equal(t, comment, test.comment)
})
}
}
func Test_toFileChecksums(t *testing.T) {
tests := []struct {
name string
digests []file.Digest
expected []model.Checksum
}{
{
name: "empty",
},
{
name: "has digests",
digests: []file.Digest{
{
Algorithm: "sha256",
Value: "deadbeefcafe",
},
{
Algorithm: "md5",
Value: "meh",
},
},
expected: []model.Checksum{
{
Algorithm: "sha256",
ChecksumValue: "deadbeefcafe",
},
{
Algorithm: "md5",
ChecksumValue: "meh",
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, toFileChecksums(test.digests))
})
}
}
func Test_fileIDsForPackage(t *testing.T) {
p := pkg.Package{
Name: "bogus",
}
c := source.Coordinates{
RealPath: "/path",
FileSystemID: "nowhere",
}
tests := []struct {
name string
id string
relationships []artifact.Relationship
expected []string
}{
{
name: "find file IDs for packages with package-file relationships",
id: string(p.ID()),
relationships: []artifact.Relationship{
{
From: p,
To: c,
Type: artifact.ContainsRelationship,
},
},
expected: []string{
string(c.ID()),
},
},
{
name: "ignore package-to-package",
id: string(p.ID()),
relationships: []artifact.Relationship{
{
From: p,
To: p,
Type: artifact.ContainsRelationship,
},
},
expected: []string{},
},
{
name: "ignore file-to-file",
id: string(p.ID()),
relationships: []artifact.Relationship{
{
From: c,
To: c,
Type: artifact.ContainsRelationship,
},
},
expected: []string{},
},
{
name: "ignore file-to-package",
id: string(p.ID()),
relationships: []artifact.Relationship{
{
From: c,
To: p,
Type: artifact.ContainsRelationship,
},
},
expected: []string{},
},
{
name: "filter by relationship type",
id: string(p.ID()),
relationships: []artifact.Relationship{
{
From: p,
To: c,
Type: artifact.OwnershipByFileOverlapRelationship,
},
},
expected: []string{},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, fileIDsForPackage(test.id, test.relationships))
})
}
}
@@ -0,0 +1,13 @@
package spdx22tagvalue
import (
"io"
"github.com/anchore/syft/syft/sbom"
"github.com/spdx/tools-golang/tvsaver"
)
func encoder(output io.Writer, s sbom.SBOM) error {
model := toFormatModel(s)
return tvsaver.Save2_2(&model, output)
}
@@ -1,17 +1,19 @@
package packages
package spdx22tagvalue
import (
"flag"
"regexp"
"testing"
"github.com/anchore/syft/internal/formats/common/testutils"
)
var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv presenters")
func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
NewSPDXTagValuePresenter(catalog, metadata),
testutils.AssertPresenterAgainstGoldenSnapshot(t,
Format().Presenter(testutils.DirectoryInput(t)),
*updateSpdxTagValue,
spdxTagValueRedactor,
)
@@ -19,9 +21,8 @@ func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
func TestSPDXTagValueImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
NewSPDXTagValuePresenter(catalog, metadata),
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
Format().Presenter(testutils.ImageInput(t, testImage, testutils.FromSnapshot())),
testImage,
*updateSpdxTagValue,
spdxTagValueRedactor,
+13
View File
@@ -0,0 +1,13 @@
package spdx22tagvalue
import "github.com/anchore/syft/syft/format"
// note: this format is LOSSY relative to the syftjson formation, which means that decoding and validation is not supported at this time
func Format() format.Format {
return format.NewFormat(
format.SPDXTagValueOption,
encoder,
nil,
nil,
)
}
@@ -0,0 +1,4 @@
# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.
FROM scratch
ADD file-1.txt /somefile-1.txt
ADD file-2.txt /somefile-2.txt
@@ -0,0 +1 @@
this file has contents
@@ -0,0 +1 @@
file-2 contents!
@@ -1,38 +1,23 @@
package packages
package spdx22tagvalue
import (
"fmt"
"io"
"time"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/formats/common/spdxhelpers"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/spdx/tools-golang/spdx"
"github.com/spdx/tools-golang/tvsaver"
)
// SPDXTagValuePresenter is a SPDX presentation object for the syft results (see https://github.com/spdx/spdx-spec)
type SPDXTagValuePresenter struct {
catalog *pkg.Catalog
srcMetadata source.Metadata
}
// NewJSONPresenter creates a new JSON presenter object for the given cataloging results.
func NewSPDXTagValuePresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *SPDXTagValuePresenter {
return &SPDXTagValuePresenter{
catalog: catalog,
srcMetadata: srcMetadata,
}
}
// Present the catalog results to the given writer.
// nolint: funlen
func (pres *SPDXTagValuePresenter) Present(output io.Writer) error {
doc := spdx.Document2_2{
// toFormatModel creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results.
// nolint:funlen
func toFormatModel(s sbom.SBOM) spdx.Document2_2 {
return spdx.Document2_2{
CreationInfo: &spdx.CreationInfo2_2{
// 2.1: SPDX Version; should be in the format "SPDX-2.2"
// Cardinality: mandatory, one
@@ -48,7 +33,7 @@ func (pres *SPDXTagValuePresenter) Present(output io.Writer) error {
// 2.4: Document Name
// Cardinality: mandatory, one
DocumentName: pres.srcMetadata.ImageMetadata.UserInput,
DocumentName: s.Source.ImageMetadata.UserInput,
// 2.5: Document Namespace
// Cardinality: mandatory, one
@@ -67,7 +52,7 @@ func (pres *SPDXTagValuePresenter) Present(output io.Writer) error {
// In many cases, the URI will point to a web accessible document, but this should not be assumed
// to be the case.
DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", pres.srcMetadata.ImageMetadata.UserInput),
DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", s.Source.ImageMetadata.UserInput),
// 2.6: External Document References
// Cardinality: optional, one or many
@@ -96,25 +81,23 @@ func (pres *SPDXTagValuePresenter) Present(output io.Writer) error {
// Cardinality: optional, one
DocumentComment: "",
},
Packages: pres.packages(),
Packages: toFormatPackages(s.Artifacts.PackageCatalog),
}
return tvsaver.Save2_2(&doc, output)
}
// packages populates all Package Information from the package Catalog (see https://spdx.github.io/spdx-spec/3-package-information/)
// nolint: funlen
func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_2 {
func toFormatPackages(catalog *pkg.Catalog) map[spdx.ElementID]*spdx.Package2_2 {
results := make(map[spdx.ElementID]*spdx.Package2_2)
for p := range pres.catalog.Enumerate() {
for p := range catalog.Enumerate() {
// name should be guaranteed to be unique, but semantically useful and stable
id := fmt.Sprintf("Package-%+v-%s", p.Type, p.Name)
// If the Concluded License is not the same as the Declared License, a written explanation should be provided
// in the Comments on License field (section 3.16). With respect to NOASSERTION, a written explanation in
// the Comments on License field (section 3.16) is preferred.
license := getSPDXLicense(p)
license := spdxhelpers.License(p)
results[spdx.ElementID(id)] = &spdx.Package2_2{
@@ -273,8 +256,8 @@ func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_
return results
}
func formatSPDXExternalRefs(p *pkg.Package) (refs []*spdx.PackageExternalReference2_2) {
for _, ref := range getSPDXExternalRefs(p) {
func formatSPDXExternalRefs(p pkg.Package) (refs []*spdx.PackageExternalReference2_2) {
for _, ref := range spdxhelpers.ExternalRefs(p) {
refs = append(refs, &spdx.PackageExternalReference2_2{
Category: string(ref.ReferenceCategory),
RefType: string(ref.ReferenceType),
+23
View File
@@ -0,0 +1,23 @@
package syftjson
import (
"encoding/json"
"fmt"
"io"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/internal/formats/syftjson/model"
)
func decoder(reader io.Reader) (*sbom.SBOM, error) {
dec := json.NewDecoder(reader)
var doc model.Document
err := dec.Decode(&doc)
if err != nil {
return nil, fmt.Errorf("unable to decode syft-json: %w", err)
}
return toSyftModel(doc)
}
+46
View File
@@ -0,0 +1,46 @@
package syftjson
import (
"bytes"
"strings"
"testing"
"github.com/anchore/syft/internal/formats/common/testutils"
"github.com/go-test/deep"
"github.com/stretchr/testify/assert"
)
func TestEncodeDecodeCycle(t *testing.T) {
testImage := "image-simple"
originalSBOM := testutils.ImageInput(t, testImage)
var buf bytes.Buffer
assert.NoError(t, encoder(&buf, originalSBOM))
actualSBOM, err := decoder(bytes.NewReader(buf.Bytes()))
assert.NoError(t, err)
for _, d := range deep.Equal(originalSBOM.Source, actualSBOM.Source) {
t.Errorf("metadata difference: %+v", d)
}
actualPackages := actualSBOM.Artifacts.PackageCatalog.Sorted()
for idx, p := range originalSBOM.Artifacts.PackageCatalog.Sorted() {
if !assert.Equal(t, p.Name, actualPackages[idx].Name) {
t.Errorf("different package at idx=%d: %s vs %s", idx, p.Name, actualPackages[idx].Name)
continue
}
for _, d := range deep.Equal(p, actualPackages[idx]) {
if strings.Contains(d, ".VirtualPath: ") {
// location.Virtual path is not exposed in the json output
continue
}
if strings.HasSuffix(d, "<nil slice> != []") {
// semantically the same
continue
}
t.Errorf("package difference (%s): %+v", p.Name, d)
}
}
}
+19
View File
@@ -0,0 +1,19 @@
package syftjson
import (
"encoding/json"
"io"
"github.com/anchore/syft/syft/sbom"
)
func encoder(output io.Writer, s sbom.SBOM) error {
doc := ToFormatModel(s)
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(&doc)
}

Some files were not shown because too many files have changed in this diff Show More