Compare commits

..
Author SHA1 Message Date
Alex Goodman 206fd8b7d3 working
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-08 08:37:40 -04:00
Alex Goodman c348d97fe0 not working
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-05 11:16:46 -04:00
Alex Goodman 30bb7cf992 pin stereoscope to dev branch
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-03 18:34:23 -04:00
Alex Goodman 54e0ee4c47 finish repository + make syft client
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-03 18:32:20 -04:00
Alex Goodman c231a9d7cb add collection
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-03 09:28:45 -04:00
Alex Goodman 3e27b6e93f rough draft for plugin system
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-02 23:34:00 -04:00
Alex Goodman 63ee5ba098 refactor catalogers to consume io.Readers instead of strings
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-31 17:41:05 -04:00
Alex Goodman df7c83c9b0 use xml encoder (#165)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-31 08:06:56 -04:00
Alex Goodman 8a4886ec0e Add package URL support to the CycloneDX presenter (#164)
* add package URL support to the CycloneDX presenter

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

* wrap license tags with licenses

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-30 21:40:19 -04:00
Alex Goodman eda0f8c774 add bom descriptor schema + test against xml schemas in pipeline (#163)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-27 19:12:45 -04:00
Alex Goodman d85d0ac418 add changelog generation (#162)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-27 08:10:56 -04:00
Alex Goodman 63661da377 Fix acceptance tests (#158)
* ensure acceptance tests fail when results are piped

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

* fix spelling in inline-compare python script

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-24 21:28:07 -04:00
Alex Goodman f892289e7c Add CycloneDX presenter (#157)
* add CycloneDX presenter + BOM Descriptor extension

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

* add docstrings to cyclonedx presenter

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-24 20:43:29 -04:00
Alex Goodman 6b65cb6d7d add release process docs (#153)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-24 14:38:31 -04:00
Alfredo Deza ae1d99b5a1 Merge pull request #150 from anchore/issue-59
Add a `setup.py` cataloger
2020-08-17 17:23:13 -04:00
Alfredo Deza 861806f1dd test: add setup.py integration cases
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-08-17 16:00:49 -04:00
Alfredo Deza 9c4024d6fa cataloger: add setup.py support for Python
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-08-17 16:00:49 -04:00
Alfredo Deza 27c207bbca pkg: add a new type to identify setup.py packages
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-08-17 10:12:27 -04:00
Alex Goodman afce14388b replace in-repo asset 2020-08-14 13:58:45 -04:00
Alex Goodman 95517d131a Improve overall documentation (#148)
* improve overall documentation

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

* fix tests to use scope.Resolver over scope

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-13 16:34:32 -04:00
Dan Luhring 47a0454084 Update dependencies (#147)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-08-13 12:12:05 -04:00
Alex Goodman f17feeb6ed Add demo gif + slack channel links (#145)
* add demo gif

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

* add slack channels to issue template selection

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

* github is picky about the issue template file extension

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-12 14:57:22 -04:00
Alex Goodman 51b9c73c31 Add documentation around catalogers, UI elements, and the event bus (#143)
* add basic documentation for catalogers (with refactoring for simplification)

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

* add docs for catalog parsers, UI, and event bus

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

* update bus phrasing

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-12 11:04:39 -04:00
Alex Goodman 5042d371cf migrate to anchore/go-rpmdb (#144)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-11 15:32:51 -04:00
Alex Goodman 90bd68e44c Disable prerelease version update check (#140)
* disable prerelease version update check

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

* use prerelease flag as source of truth for user notifications

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-11 09:54:04 -04:00
Alfredo Deza 691e26d3a3 Merge pull request #142 from anchore/args-fix
cmd: allow no args passed, don't error
2020-08-11 08:29:28 -04:00
Alfredo Deza 3bcae045cd cmd: allow no args passed, don't error
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-08-11 08:01:19 -04:00
Ross Turk d7695aeae1 Add beta warning and todos (#136)
* Add future ideas + beta warning to README

Signed-off-by: Ross Turk <ross@rossturk.com>

* Update link

Signed-off-by: Ross Turk <ross@rossturk.com>
2020-08-11 07:07:55 -04:00
Dan Luhring 73ec35f513 Update installation method (#139)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-08-10 19:29:19 -04:00
Alex Goodman 676544b6ab use token on release
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-10 18:28:43 -04:00
Alex Goodman 647c806ee4 Add install script + brew tap (#138)
* add install script

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

* add brew tap

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

* rm install warning

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

* force non-prereleases

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-10 17:15:00 -04:00
Dan Luhring 89dc415e49 Add badges (#137)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-08-10 16:46:12 -04:00
Alex Goodman ac9141e2ad add issue template (#135)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-10 14:47:32 -04:00
Alfredo Deza 21aa32c599 Merge pull request #134 from anchore/issue-69
docs: add contributing guidelines
2020-08-10 14:46:54 -04:00
Alfredo Deza 2adbe75002 docs: add contributing guidelines
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-08-10 14:15:07 -04:00
Alex Goodman 2d452bf59e Add inline-comparison as acceptance test (#130)
* add inline-compare as acceptance test

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

* add additional RPM metadata

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

* add comments and doc strings to the compare-* make targets

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-10 10:33:44 -04:00
Dan Luhring 4c7784da62 Add shell completion script (#131)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-08-10 09:31:02 -04:00
Ross Turk 86d3336280 Add macos quarantine to readme (#129)
* Add note about macOS quarantine

Signed-off-by: Ross Turk <ross@rossturk.com>

* Fix project name

Signed-off-by: Ross Turk <ross@rossturk.com>
2020-08-07 16:47:36 -04:00
Alex Goodman a3a3e3848f replace master with main (#128)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-07 13:27:11 -04:00
Alex Goodman fa5d2b5d98 fix readme installation notice
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-07 11:42:58 -04:00
Alex Goodman 817ce61036 Add detailed location info to json artifact (#127)
* add detailed location info to json artifact

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

* decompose json presenter

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-08-07 10:05:16 -04:00
Alex Goodman dc8dfc8457 fix panic on top-level log (#125) 2020-08-06 08:29:52 -04:00
Alex Goodman f855a38a01 pull all commits on checkout for release to build changelog (#126) 2020-08-06 08:28:01 -04:00
Alex Goodman bfc5dd87af replace fetching->loading and reading->parsing in UI (#124) 2020-08-06 08:19:03 -04:00
Dan Luhring 70e673204c Add poetry cataloger (#121)
* Minor cleanup

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Update pkg Type definition to string

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Implement poetry.lock parsing

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Address CI issues

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>

* Integrate Alex's changes

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-08-04 18:22:43 -04:00
Alex Goodman e2a874a277 finalize json output & add schema (#118) 2020-08-04 16:05:53 -04:00
Alex GoodmanandAlfredo Deza 2560266e38 Initial README (#120)
* initial readme + logging and output fixes to support descriptions

* minor tweaks to README descriptions

* simplify phrasing

Co-authored-by: Alfredo Deza <adeza@anchore.com>

* add comment for example gif

Co-authored-by: Alfredo Deza <adeza@anchore.com>

* spelling fix

Co-authored-by: Alfredo Deza <adeza@anchore.com>

* update phrasing

Co-authored-by: Alfredo Deza <adeza@anchore.com>

* simplify TODO note

Co-authored-by: Alfredo Deza <adeza@anchore.com>

* grammar fix

Co-authored-by: Alfredo Deza <adeza@anchore.com>

* phrase fix

Co-authored-by: Alfredo Deza <adeza@anchore.com>

* rm readme bash block indicators

* clarify config examples are default values

* rm value

Co-authored-by: Alfredo Deza <adeza@anchore.com>

Co-authored-by: Alfredo Deza <adeza@anchore.com>
2020-08-04 15:22:34 -04:00
Alex Goodman 8fe59c6f1a bump stereoscope for docker pull + add UI elements for pull status (#117) 2020-08-03 16:03:47 -04:00
Alex Goodman 78515da285 replace zap logger with logrus (#116) 2020-08-01 11:56:38 -04:00
Alex Goodman 076d5c2f1a fix ui handlers to write before first event 2020-07-31 17:53:22 -04:00
Alex Goodman 5320280216 show message when no packages are discovered (#115) 2020-07-31 08:30:35 -04:00
Alfredo Deza c67e17a2cf Merge pull request #114 from anchore/issue-111
Add another option to fall back on name detection
2020-07-31 07:50:49 -04:00
Alfredo Deza 04a1c91881 java: fallback to manifest.ImplTitle when there is no name
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-31 07:35:43 -04:00
Alfredo Deza bb81c0b9fe tests: java cataloger tests for selecting name
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-31 07:35:43 -04:00
Alex Goodman e3976596f3 pull in fix for bounds check progress formatting values in etui 2020-07-31 06:56:20 -04:00
Alex Goodman 271ba35c85 Export UI handlers for reuse in other tools (#113)
* export UI handlers for reuse in other tools

* bump jotframe to fix cursor issues at frame close
2020-07-30 15:16:58 -04:00
Alex Goodman 857f41b04b Merge pull request #112 from anchore/ignore-prerelease-versions
Ignore prerelease versions when uploading version file on release
2020-07-30 12:37:25 -04:00
Alex Goodman ad1a72c6ff ignore prerelease verions when uploading version file on release 2020-07-29 14:54:47 -04:00
Alfredo Deza bc693829a6 Merge pull request #110 from anchore/issue-8
adds the yarn.lock cataloger
2020-07-29 11:20:06 -04:00
Alfredo Deza caecce9b41 tests: update integration tests to include yarn packages
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-28 15:35:45 -04:00
Alfredo Deza 713f660528 cataloger: update controller to use javascript (vs. npm)
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-28 15:20:11 -04:00
Alfredo Deza d79cecea3e tests: verify new yarn.lock parser
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-28 15:20:11 -04:00
Alfredo Deza 57904743aa pkg: define the Yarn package type
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-28 15:18:47 -04:00
Alfredo Deza 67fb1326e0 cataloger: implement the yarn.lock parser
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-28 15:18:47 -04:00
Alfredo Deza 146b4bd01f cataloger: rename npm to javascript to accommodate yarn parser
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-28 15:18:47 -04:00
Alex Goodman 06f8355fce finalize update check URL 2020-07-27 15:13:41 -04:00
Alex Goodman 19eadde9ca use aws creds for version file upload 2020-07-27 09:55:11 -04:00
Alex Goodman d21de64cb3 use mount path for version upload 2020-07-27 09:35:55 -04:00
Alex Goodman 50b50a50b2 Merge pull request #108 from anchore/upload-version
Upload version check file on release
2020-07-27 09:33:52 -04:00
Alex Goodman 6536f0bb36 upload version check file on release 2020-07-27 09:30:05 -04:00
Alex Goodman 2e15d854eb bump circle resources 2020-07-25 19:08:19 -04:00
Alex Goodman 78c3652759 bump bouncer version 2020-07-25 16:55:41 -04:00
Alex Goodman 2502814143 rollup static analysis to make target 2020-07-25 16:40:37 -04:00
Alex Goodman 991bf203da bust old imgbom cache 2020-07-25 10:36:17 -04:00
Alex Goodman 05c78de9d3 generate java fixtures ahead of tests 2020-07-25 10:06:52 -04:00
Alex Goodman c9dea59232 verify signing fingerprint 2020-07-25 09:59:48 -04:00
Alex Goodman 08ea496544 Merge pull request #94 from anchore/table-output
Add default table presenter
2020-07-25 08:49:14 -04:00
Alex Goodman 1ba0678cf6 provide signed checksums 2020-07-25 08:42:50 -04:00
Alex Goodman c814379c80 add auto prerelease option 2020-07-25 07:33:24 -04:00
Alex Goodman f3428e49b8 add release quality gate 2020-07-25 07:23:15 -04:00
Alex Goodman 585569e929 fix gha go cache key; rm brew until oss release 2020-07-25 07:18:21 -04:00
Alex Goodman 32bd57886e add publish release 2020-07-25 07:09:20 -04:00
Alex Goodman 44f26c7f90 update release quality gate version 2020-07-25 07:03:57 -04:00
Alfredo DezaandAlex Goodman f8a5c56191 Fix acceptance tests (#106)
* test: allow no metadata in packages

Signed-off-by: Alfredo Deza <adeza@anchore.com>

* test: update centos json fixture

Signed-off-by: Alfredo Deza <adeza@anchore.com>

Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2020-07-25 06:39:44 -04:00
Alex Goodman 2e458cd73d make acceptance test results dir on run 2020-07-24 18:04:52 -04:00
Alex Goodman 6b0ab2de4a Merge pull request #107 from anchore/fix-acceptance-compare
Fix acceptance test compare script & persist results
2020-07-24 17:53:02 -04:00
Alex Goodman 14ec30aee1 fix acceptance test compare script & persist 2020-07-24 17:41:22 -04:00
Alfredo Deza 578952986f Merge pull request #103 from anchore/issue-81
scope: do not create scope if path is invalid
2020-07-24 15:33:30 -04:00
Alfredo Deza e1ce040ead scope: do not create scope if path is invalid
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-24 15:25:14 -04:00
Alex Goodman 9ec5da24dd fix py globs to include partial matches (#101) 2020-07-24 08:22:56 -04:00
Alex Goodman 32071b0bf1 add default table presenter 2020-07-23 21:48:12 -04:00
Alex Goodman 0a0bc68e95 show verbose test output to prevent circleci kills 2020-07-23 21:47:11 -04:00
Alex Goodman a4016d35ce rename to syft 2020-07-23 20:54:04 -04:00
Alex Goodman 2132700198 add apk/alpine support (#98) 2020-07-23 20:35:57 -04:00
Alex Goodman 63ba7ae47d add remaining debian & el distro support (#99) 2020-07-23 20:35:40 -04:00
Alex Goodman 5ccd6d5f6a check for unsupported "go get" chars (#100) 2020-07-23 13:08:31 -04:00
Alex GoodmanandAlfredo Deza ba4f63099d Add release process (#89)
* add check for app update; fix ETUI error handling

* validate user args

* add goreleaser support

* replace cgo dependencies (go-rpm) with go equivalents

* add acceptance tests against build snapshot

* add brew tap + acceptance test pipeline

* add mac acceptance tests

* fix compare makefile

* fix mac acceptance tests

* add release pipeline with wait checks

* add token to release step

* rm dir presenters int test

* enforce dpkg to be non interactive

Co-authored-by: Alfredo Deza <adeza@anchore.com>

* pin brew formulae

* pin skopeo to formulae url

* only run acceptance tests

Co-authored-by: Alfredo Deza <adeza@anchore.com>
2020-07-23 10:52:44 -04:00
Alfredo Deza 3cb7c43dbc Merge pull request #96 from anchore/issue-7
Add support for analyzing package-lock.json files
2020-07-23 09:54:39 -04:00
Alfredo Deza f1c14d97f6 integration: validate package-lock.json parsing
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-23 09:51:05 -04:00
Alfredo Deza 9f314eb493 cataloger: support npm packages from package-lock.json files
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-23 09:51:05 -04:00
Alfredo Deza fb4b83cf3e cataloger: define Javascript (npm) package type
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-23 09:51:01 -04:00
Alex Goodman 44c69f1f91 add go.mod cataloger (#97) 2020-07-23 08:17:30 -04:00
Alfredo Deza 265516682f Merge pull request #92 from anchore/issue-58
catalog Python packages from a requirements.txt file
2020-07-21 16:00:20 -04:00
Alfredo Deza 74d87254ed integration: validate requirements.txt is picked up and parsed
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-21 15:49:19 -04:00
Alfredo Deza 50eae46d37 type: add PythonPkg types for requirements-based packages
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-21 15:49:19 -04:00
Alfredo Deza f8a2b7a626 tests: verify requirements.txt captures versions
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-21 15:49:19 -04:00
Alfredo Deza 83056a4b6f cataloger: add a requirements.txt parser
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-21 15:38:30 -04:00
Alfredo Deza 8747b68b86 cataloger: add a glob path for parsing requirements.txt
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-21 14:37:06 -04:00
Alex Goodman 7ebb9f4e0b Add check for app update (#88)
* add check for app update; fix ETUI error handling

* validate user args
2020-07-21 12:02:03 -04:00
Alfredo Deza 380cd39514 Merge pull request #91 from anchore/issue-75
Recurse when creating a directory catalog
2020-07-21 11:26:03 -04:00
Alfredo Deza cea38fcba3 cataloger: use ** for glob patterns
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-21 11:19:42 -04:00
Alfredo Deza 03bbcfa08d cataloger: add ability to recurse with doublestar
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-21 11:10:02 -04:00
Alfredo Deza 942cd6eb18 dependencies: add doublestar for recursive globbing support
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-21 11:10:02 -04:00
Alfredo Deza f02b8cc7bc tests: use the catalog directly instead of via the presenters
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-21 11:10:02 -04:00
Alfredo Deza fe15c0a08b tests: remove dir_presenters, no longer needed
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-20 15:55:52 -04:00
Alex Goodman 4fa391b4cb remove metadata from text presenter (#90) 2020-07-20 15:28:26 -04:00
Alex Goodman 9e285fd0e2 use common entry point for integration tests; refactor cmd pkg (#86) 2020-07-17 15:16:33 -04:00
Alfredo Deza b5a353349f Merge pull request #87 from anchore/unkown-distro
Add a helper function/constructor for an UnkownDistro
2020-07-17 13:54:14 -04:00
Alfredo Deza b734623ef0 make: bump test coverage
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-17 13:32:53 -04:00
Alfredo Deza 0534a68ec6 tests for new Identify distro func
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-17 13:32:53 -04:00
Alfredo Deza e2b71184aa when no distros are matched, return an UnkownDistro
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-17 13:32:53 -04:00
Alfredo Deza f51cae2043 distro: add new constructor for getting UnkownDistros
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-17 13:32:53 -04:00
Alfredo Deza f03f7b32e9 Merge pull request #85 from anchore/issue-84
Remove carriage returns from distro files
2020-07-17 08:31:22 -04:00
Alfredo Deza fb6bc116e0 tests: verify carriage returns are parsed in distro files
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-16 17:20:30 -04:00
Alfredo Deza 5edcae13b3 distro: remove unwanted chars from os distro files
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-16 17:19:57 -04:00
Alfredo Deza aa0693bdc0 Merge pull request #83 from anchore/issue-82
Do not join paths when returning multiple file contents by ref
2020-07-16 14:36:30 -04:00
Alfredo Deza a05d58686e resolvers: do not join paths, it is assumed they are already joined
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-16 13:14:03 -04:00
Alfredo Deza d82a0872c9 tests: verify MultipleFileContentsByRef gets contents from the right path
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-16 13:14:03 -04:00
Alfredo Deza 07b2a1da87 Merge pull request #76 from anchore/issue-73
Better abstraction for img/dir decision fork
2020-07-15 10:49:15 -04:00
Alfredo Deza b457d4ebd2 tests: drop coverage requirement to 69% for now
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 10:15:58 -04:00
Alfredo Deza d982ad6345 lib: implement a NewScope to produce scopes from inputs
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 10:15:58 -04:00
Alfredo Deza b2c8685437 integration: use new constructor params for scope from images
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 10:15:58 -04:00
Alfredo Deza f418c99a3b remove old text support files
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 10:15:58 -04:00
Alfredo Deza 45b5fa82c8 presenters: abstract into text only
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 10:15:58 -04:00
Alfredo Deza e7518ad998 tests: move img-based tests and fixtures
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 09:59:40 -04:00
Alfredo Deza a6e1866cdb cmd: do not default for images, handle it specifically
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 09:59:40 -04:00
Alfredo Deza 2f626bf9fd directory resolver should join path and target directory for retrieving contents
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 09:59:40 -04:00
Alfredo Deza 429c28720c integration: update tests to use scope instead of img directly
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 07:53:28 -04:00
Alfredo Deza 69b01caba7 update scope tests to use new generic scope
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 07:53:28 -04:00
Alfredo Deza c56b82e529 resolvers: add a directory resolver
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 07:53:28 -04:00
Alfredo Deza 5cee2668e3 resolvers: satisfy interface with multiple file contents by ref
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 07:53:28 -04:00
Alfredo Deza 6e9171e8a6 presenter: use new scoped source
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 07:53:27 -04:00
Alfredo Deza 4b69758f2c scope: create a new generic Resolver interface
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 07:53:27 -04:00
Alfredo Deza b35a412467 scope: use a generic scope struct instead of specific to images
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 07:53:27 -04:00
Alfredo Deza 145bb95a2d lib: use scope instead of requiring images
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 07:53:27 -04:00
Alfredo Deza d22c1d0838 distro: identify uses the scope resolvers now, not images
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 07:53:27 -04:00
Alfredo Deza 923d220a90 controller: use new scope.Resolver interface
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 07:53:27 -04:00
Alfredo Deza 6cbf6d3409 cmd: do not pass img around, use scope
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-15 07:53:27 -04:00
Alex Goodman 66a16a67fd Add support for detecting nested java archives (#77)
* with sb build app

* test nested jar support

* pin jdk version during parse test (but dont compare version)
2020-07-15 07:42:35 -04:00
Alex Goodman e8d11eec69 add license validation (#80) 2020-07-13 13:07:20 -04:00
Alex GoodmanandAlfredo Deza 61f51d80bb Add comparative analysis with anchore-engine (#78)
* add comparative analysis

* remove extra comma from compare script tuple

Co-authored-by: Alfredo Deza <adeza@anchore.com>

Co-authored-by: Alfredo Deza <adeza@anchore.com>
2020-07-13 12:12:00 -04:00
Alex Goodman 3f090f9647 fix java cache key for CI; fix circle docker api version (#79) 2020-07-13 12:11:11 -04:00
Alex Goodman 502e2afd06 fix linting issue 2020-07-10 07:45:13 -04:00
Alex Goodman 15ff94b56b tweak java manifest version processing 2020-07-10 07:41:13 -04:00
Alex Goodman ef8c215772 Merge pull request #74 from anchore/add-java-cataloger
Adds java cataloger
2020-07-09 17:05:29 -04:00
Alex Goodman 4c7d9ccef7 simplify unzip; update java error statements 2020-07-09 11:53:52 -04:00
Alex GoodmanandAlfredo Deza 0f8dcf3f17 Update imgbom/cataloger/java/pom_properties.go
Co-authored-by: Alfredo Deza <adeza@anchore.com>
2020-07-09 11:38:02 -04:00
Alex GoodmanandAlfredo Deza c9bfc0aecd Update imgbom/cataloger/java/pom_properties.go
Co-authored-by: Alfredo Deza <adeza@anchore.com>
2020-07-09 11:37:45 -04:00
Alex GoodmanandAlfredo Deza 40f8e3a7de Update imgbom/cataloger/java/java_manifest.go
Co-authored-by: Alfredo Deza <adeza@anchore.com>
2020-07-09 11:31:23 -04:00
Alex Goodman 1f89d26902 reduce java packages to java-archive and jenkins-plugin 2020-07-09 10:46:28 -04:00
Alex Goodman e55db9247e add java cataloger 2020-07-08 16:16:01 -04:00
Alfredo Deza 1d711ee06f Merge pull request #71 from anchore/protocol-cleanup
cmd: replace Printf with a logging statement
2020-07-07 10:42:26 -04:00
Alfredo Deza 74f339e043 cmd: replace Printf with a logging statement
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-07 10:40:55 -04:00
Alex Goodman e040fc8930 Merge pull request #72 from anchore/add-rpm-cataloger
Add RPM DB support
2020-07-07 09:06:54 -04:00
Alex Goodman 1896831c39 add rpmdb support; enhance integration tests 2020-07-06 12:55:11 -04:00
Alex Goodman 7cd16d05b8 Merge pull request #70 from anchore/add-coverage
Add test coverage (+ makefile improvements)
2020-07-06 12:45:52 -04:00
Alex Goodman d59a19697d add coverage + makefile improvements 2020-07-06 07:25:18 -04:00
Alex Goodman 027f71de45 fix env config reads for nested config values 2020-07-03 12:00:46 -04:00
Alfredo Deza fe338760b0 Merge pull request #68 from anchore/issue-31-1
Add support for file-based contents
2020-07-02 17:29:45 -04:00
Alfredo Deza 9baae90c0b integration: add tests for dir-based presenters
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 16:09:55 -04:00
Alfredo Deza 3548c7b132 tests for scope.go
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 16:09:55 -04:00
Alfredo Deza 3eedb86a1f tests for protocol.go
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 16:09:55 -04:00
Alfredo Deza c35108b00d tests: reorganize text img files to match new layout
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 16:09:55 -04:00
Alfredo Deza f49ca8107a tests: add verification for dirs and imgs
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 16:09:55 -04:00
Alfredo Deza 293b7b6ff9 tests: update integration tests to use new cataloger
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 16:09:55 -04:00
Alfredo Deza 96d86fa214 tests: move existing json tests to json/imgs
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 16:09:55 -04:00
Alfredo Deza 7544330bc2 json img presenter needs to degrade nicely when file metadata is not present
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 15:12:48 -04:00
Alfredo Deza feef7e28dc cmd: add note about panic when img is nil
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 15:12:48 -04:00
Alfredo Deza ec52e91301 scope: create interface for content and file resolvers
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 15:12:48 -04:00
Alfredo Deza b2d7bd3b1a scope: add directory-based scoping
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 15:12:48 -04:00
Alfredo Deza e38e3e9ff4 create text-based presenters for dirs and imgs
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 15:12:48 -04:00
Alfredo Deza 268cfef0ee re-organize json-based presenters for dirs and imgs
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 15:12:48 -04:00
Alfredo Deza 312c8acfbe presenter can pick text-dir, text-img, json-text, or json-img for reporting
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 15:12:48 -04:00
Alfredo Deza 69a0376f99 distro: add note to replace with MultipleFileContents in the future
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 15:12:48 -04:00
Alfredo Deza 507a6b69cb catalog controller uses scope to determine file resolvers
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 15:12:48 -04:00
Alfredo Deza faf21a67a6 lib: return a protocol-based scope (dir or img)
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 15:12:48 -04:00
Alfredo Deza 678a2c1dcb create a protocol-parsing package to distinguish dir,docker, etc...
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-02 13:04:19 -04:00
Alfredo Deza 4d31655908 allow root command to catalog a directory
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-07-01 07:35:01 -04:00
Alex Goodman 466169e8d7 Merge pull request #65 from anchore/add-an-event-bus
Add and use event bus for UI progress
2020-06-25 11:04:45 -04:00
Alex Goodman 926b5f2a50 add and use event bus for UI progress 2020-06-25 10:39:11 -04:00
Alex Goodman 2e9e969c5f Merge pull request #66 from anchore/add-license
Add apache v2 license
2020-06-24 14:51:51 -04:00
Alex Goodman 1569a85820 add apache v2 license 2020-06-24 14:37:00 -04:00
Alfredo Deza d2e6229b2e Merge pull request #64 from anchore/issue-36
Create a text presenter, default to it
2020-06-22 14:05:06 -04:00
Alfredo Deza 0a1f99503f cli: default to the new text presenter
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-06-22 11:09:00 -04:00
Alfredo Deza 2e6aef17e1 implement a text presenter
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-06-22 11:09:00 -04:00
Alfredo Deza 72f9091493 presenter: allow a text presenter option
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-06-19 15:46:31 -04:00
Alfredo Deza acbeea9800 option: add a Text-based presenter
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-06-19 15:45:58 -04:00
Alex Goodman 2cb7dad967 Merge pull request #63 from anchore/add-symlink-suport
Add symlink support
2020-06-16 13:10:24 -04:00
Alex Goodman cd94179985 add symlink content fetching support 2020-06-16 11:37:37 -04:00
Alex Goodman 778754dd59 Merge pull request #62 from anchore/add-simple-pipeline
Add basic pipeline
2020-06-15 13:53:49 -04:00
Alex Goodman 6b5a22da46 add basic pipeline 2020-06-15 13:51:07 -04:00
Alex Goodman 0d66cb8a1c Merge pull request #60 from anchore/rename-analyzers
Rename analyzers to catalogers
2020-06-15 13:18:07 -04:00
Alex Goodman 294e267e47 rename analyzers to catalogers; fix generic cataloger path dispatcher 2020-06-15 10:52:55 -04:00
Alfredo Deza 812934c964 Merge pull request #57 from anchore/issue-33
Initial take on distro detection
2020-06-12 15:58:36 -04:00
Alfredo Deza e460c031b6 distro: add tests and fixtures for identification
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-06-12 15:57:51 -04:00
Alfredo Deza 889fa52b7d lib: update signature to *Distro
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-06-12 10:49:14 -04:00
Alfredo Deza 75375d0b58 cmd: IdentifyDistro returns a distro object which can be nil
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-06-12 10:49:14 -04:00
Alfredo Deza dba5f0b197 initial take on distro identification
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-06-12 10:49:14 -04:00
Alfredo Deza 56c2318ea1 add support for distro detection
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-06-12 10:49:14 -04:00
Alfredo Deza f1851556a8 distro: add a Name stringer
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-06-12 10:49:14 -04:00
Alex Goodman 64a9125895 Merge pull request #53 from anchore/add-python-wheel-egg-analyzers
Add python wheel and egg analyzer support
2020-06-05 09:59:27 -04:00
Alex Goodman a0ccfe220b add python wheel and egg analyzer support 2020-06-05 08:47:17 -04:00
Alex Goodman 83e96e8880 generalize common analyzer elements 2020-06-04 17:49:52 -04:00
Alex Goodman e88669c536 Merge pull request #52 from anchore/add-gem-analyzer
Add gem analyzer
2020-06-04 14:43:52 -04:00
Alex Goodman f33720eaa1 add gem analyzer 2020-06-04 14:42:59 -04:00
Alex Goodman 033c91a233 Merge pull request #35 from anchore/use-filetree-reader
Utilize filetree reader
2020-06-04 14:36:40 -04:00
Alex Goodman b6122a413b add distro version tests 2020-06-02 20:48:15 -04:00
Alex Goodman cc724bff26 use filetree reader instead of filetree 2020-06-02 08:52:26 -04:00
Alex Goodman f0b8aaacda Merge pull request #34 from anchore/focus-dkpg-metadata
Add dpkg source field to metadata
2020-06-01 17:42:18 -04:00
Alex Goodman 17787f856f rm unused dkpg metadata, add dpkg source field, add analyzer source name 2020-06-01 15:25:47 -04:00
Alex Goodman 83e486d6ca Merge pull request #29 from anchore/add-os-types
Start of distro identification support
2020-06-01 10:47:31 -04:00
Alex Goodman 45fbd05cf9 add first distro support 2020-06-01 10:46:06 -04:00
Alex Goodman 1f272c2bda Merge pull request #27 from anchore/add-package-id
Add package id
2020-06-01 08:55:22 -04:00
Alex Goodman a0e48468c2 with package id 2020-05-29 16:06:31 -04:00
330 changed files with 26835 additions and 1519 deletions
+9
View File
@@ -0,0 +1,9 @@
permit:
- BSD.*
- MIT.*
- Apache.*
- MPL.*
- ISC
ignore-packages:
# packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE
- github.com/package-url/packageurl-go
+129
View File
@@ -0,0 +1,129 @@
version: 2.1
jobs:
run-static-analysis:
parameters:
version:
type: string
docker:
- image: circleci/golang:<< parameters.version >>
environment:
GO111MODULE: "on"
# work around for recent circle CI breaking change
# Error: "Error response from daemon: client version 1.39 is too new. Maximum supported API version is 1.38"
DOCKER_API_VERSION: "1.38"
# 2CPU / 4GB RAM
resource_class: medium
steps:
- checkout
- restore_cache:
keys:
- go-<< parameters.version >>-{{ checksum "go.sum" }}-{{ checksum "Makefile" }}
- run: make ci-bootstrap
- save_cache:
key: go-<< parameters.version >>-{{ checksum "go.sum" }}-{{ checksum "Makefile" }}
paths:
- "/go/pkg/mod"
- ".tmp"
- run:
name: run static analysis
command: make static-analysis
run-tests:
parameters:
version:
type: string
docker:
- image: circleci/golang:<< parameters.version >>
environment:
GO111MODULE: "on"
# work around for recent circle CI breaking change
# Error: "Error response from daemon: client version 1.39 is too new. Maximum supported API version is 1.38"
DOCKER_API_VERSION: "1.38"
# 2CPU / 4GB RAM
resource_class: medium
steps:
- checkout
- restore_cache:
keys:
- go-<< parameters.version >>-{{ checksum "go.sum" }}-{{ checksum "Makefile" }}
- run: make ci-bootstrap
- save_cache:
key: go-<< parameters.version >>-{{ checksum "go.sum" }}-{{ checksum "Makefile" }}
paths:
- "/go/pkg/mod"
- ".tmp"
- setup_remote_docker:
version: 18.06.0-ce
- run:
name: enable docker client
command: |
# all of this to enable "circleci local execute ..." cli commands for /var/run/docker.sock
mkdir -p ${HOME}/.local/bin
cat \<< EOF > ${HOME}/.local/bin/docker
#!/bin/bash
set -xue
sudo -E ${HOME}/.local/bin/docker.bin \$@
EOF
sudo mv /usr/bin/docker ${HOME}/.local/bin/docker.bin
chmod 755 ${HOME}/.local/bin/docker
- run:
name: build cache key for java test-fixture blobs
command: make java-packages-fingerprint
- restore_cache:
keys:
- syft-unit-test-java-cache-{{ checksum "syft/cataloger/java/test-fixtures/java-builds/packages.fingerprint" }}
- run:
name: run unit tests
command: make unit
- save_cache:
key: syft-unit-test-java-cache-{{ checksum "syft/cataloger/java/test-fixtures/java-builds/packages.fingerprint" }}
paths:
- "syft/cataloger/java/test-fixtures/java-builds/packages"
- run:
name: validate syft output against the CycloneDX schema
command: make validate-cyclonedx-schema
- run:
name: build hash key for integration test-fixtures blobs
command: make integration-fingerprint
- restore_cache:
keys:
- syft-integration-test-tar-cache-{{ checksum "test/integration/test-fixtures/tar-cache.fingerprint" }}
- run:
name: run integration tests
command: make integration
- save_cache:
key: syft-integration-test-tar-cache-{{ checksum "test/integration/test-fixtures/tar-cache.fingerprint" }}
paths:
- "test/integration/test-fixtures/tar-cache"
workflows:
# Note: changing this workflow name requires making the same update in the .github/workflows/release.yaml pipeline
"Static Analysis + Unit + Integration":
jobs:
- run-static-analysis:
name: "Static Analysis"
version: "1.14"
- run-tests:
name: "Unit & Integration Tests (go-1.13)"
version: "1.13"
- run-tests:
name: "Unit & Integration Tests (go-latest)"
version: "latest"
+20
View File
@@ -0,0 +1,20 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**What happened**:
**What you expected to happen**:
**How to reproduce it (as minimally and precisely as possible)**:
**Anything else we need to know?**:
**Environment**:
- syft version (use `syft version -v`):
- OS (e.g: `cat /etc/os-release` or similar):
+11
View File
@@ -0,0 +1,11 @@
contact_links:
- name: Help & Support Slack Channel ⛑️
# link to toolbox-help channel
url: https://anchorecommunity.slack.com/archives/C019BUXV7R6
about: Ask a questions and get answers
- name: Developer Slack Channel 💬
# link to toolbox-dev channel
url: https://anchorecommunity.slack.com/archives/C0190NF9TSM
about: Participate in design discussions and feature development
+15
View File
@@ -0,0 +1,15 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**What would you like to be added**:
**Why is this needed**:
**Additional context**:
<!-- Add any other context or screenshots about the feature request here. -->
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
set -ue
BIN="syft"
DISTDIR=$1
VERSION=$2
# the source of truth as to whether we want to notify users of an update is if the release just created is NOT
# flagged as a pre-release on github
if [[ "$(curl -SsL https://api.github.com/repos/anchore/${BIN}/releases/tags/${VERSION} | jq .prerelease)" == "true" ]] ; then
echo "skipping publishing a version file (this is a pre-release: ${VERSION})"
exit 0
fi
echo "creating and publishing version file"
# create a version file for version-update checks
echo "${VERSION}" | tee ${DISTDIR}/VERSION
# upload the version file that supports the application version update check
docker run --rm \
-i \
-e AWS_DEFAULT_REGION=us-west-2 \
-e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \
-e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \
-v $(pwd)/${DISTDIR}/:/distmount \
amazon/aws-cli \
s3 cp /distmount/VERSION s3://toolbox-data.anchore.io/${BIN}/releases/latest/VERSION
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -ue
DISTDIR=$1
export FINGERPRINT=$(gpg --verify ${DISTDIR}/*checksums.txt.sig ${DISTDIR}/*checksums.txt 2>&1 | grep 'using RSA key' | awk '{ print $NF }')
if [[ "${FINGERPRINT}" == "${SIGNING_FINGERPRINT}" ]]; then
echo 'verified signature'
else
echo "signed with unknown fingerprint: ${FINGERPRINT}"
echo " expected fingerprint: ${SIGNING_FINGERPRINT}"
exit 1
fi
+123
View File
@@ -0,0 +1,123 @@
name: 'Acceptance'
on:
workflow_dispatch:
push:
# ... only act on pushes to main
branches:
- main
# ... do not act on release tags
tags-ignore:
- v*
env:
GO_VERSION: "1.14.x"
jobs:
Build-Snapshot-Artifacts:
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
- name: Restore bootstrap cache
id: cache
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
${{ github.workspace }}/.tmp
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: Bootstrap dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: make ci-bootstrap
- name: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v2
env:
GPG_PRIVATE_KEY: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }}
PASSPHRASE: ${{ secrets.SIGNING_GPG_PASSPHRASE }}
- name: GPG signing info
run: |
echo "fingerprint: ${{ steps.import_gpg.outputs.fingerprint }}"
echo "keyid: ${{ steps.import_gpg.outputs.keyid }}"
echo "name: ${{ steps.import_gpg.outputs.name }}"
echo "email: ${{ steps.import_gpg.outputs.email }}"
- name: Build snapshot artifacts
run: make snapshot
env:
GPG_PRIVATE_KEY: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }}
PASSPHRASE: ${{ secrets.SIGNING_GPG_PASSPHRASE }}
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: snapshot/**/*
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Acceptance-Linux:
needs: [ Build-Snapshot-Artifacts ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Run Acceptance Tests (Linux)
run: make acceptance-linux
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Acceptance-Mac:
needs: [ Build-Snapshot-Artifacts ]
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Run Acceptance Tests (Mac)
run: make acceptance-mac
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Inline-Compare:
needs: [ Build-Snapshot-Artifacts ]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Fingerprint inline-compare sources
run: make compare-fingerprint
- name: Restore inline reports cache
id: cache
uses: actions/cache@v2
with:
path: ${{ github.workspace }}/test/inline-compare/inline-reports
key: inline-reports-${{ hashFiles('**/inline-compare.fingerprint') }}
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Compare Anchore inline-scan results against snapshot build output
run: make compare-snapshot
+129
View File
@@ -0,0 +1,129 @@
name: 'Release'
on:
push:
# take no actions on push to any branch...
branches-ignore:
- '**'
# ... only act on release tags
tags:
- 'v*'
env:
GO_VERSION: "1.14.x"
jobs:
wait-for-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# we don't want to release commits that have been pushed and tagged, but not necessarily merged onto main
- name: Ensure tagged commit is on main
run: |
echo "Tag: ${GITHUB_REF##*/}"
git fetch origin main
git merge-base --is-ancestor ${GITHUB_REF##*/} origin/main && echo "${GITHUB_REF##*/} is a commit on main!"
- name: Check static anaylysis, unit, and integration test results
uses: fountainhead/action-wait-for-check@v1.0.0
id: sa-unit-int
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the circle-ci workflow name (in .circleci/config.yaml)
checkName: "Static Analysis + Unit + Integration"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (linux)
uses: fountainhead/action-wait-for-check@v1.0.0
id: acceptance-linux
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
checkName: "Acceptance-Linux"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (mac)
uses: fountainhead/action-wait-for-check@v1.0.0
id: acceptance-mac
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
checkName: "Acceptance-Mac"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check inline comparison test results
uses: fountainhead/action-wait-for-check@v1.0.0
id: inline-compare
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
checkName: "Inline-Compare"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Quality gate
if: steps.sa-unit-int.outputs.conclusion != 'success' || steps.inline-compare.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success'
run: |
echo "Static/Unit/Integration Status: ${{ steps.sa-unit-int.outputs.conclusion }}"
echo "Acceptance Test (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}"
echo "Acceptance Test (Mac) Status: ${{ steps.acceptance-mac.outputs.conclusion }}"
echo "Inline Compare Status: ${{ steps.inline-compare.outputs.conclusion }}"
false
release:
needs: [ wait-for-checks ]
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Restore bootstrap cache
id: cache
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
${{ github.workspace }}/.tmp
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: Bootstrap dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: make ci-bootstrap
- name: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v2
env:
GPG_PRIVATE_KEY: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }}
PASSPHRASE: ${{ secrets.SIGNING_GPG_PASSPHRASE }}
- name: GPG signing info
run: |
echo "fingerprint: ${{ steps.import_gpg.outputs.fingerprint }}"
echo "keyid: ${{ steps.import_gpg.outputs.keyid }}"
echo "name: ${{ steps.import_gpg.outputs.name }}"
echo "email: ${{ steps.import_gpg.outputs.email }}"
- name: Build & publish release artifacts
run: make release
env:
GITHUB_TOKEN: ${{ secrets.ANCHORE_GIT_READ_TOKEN }}
GPG_PRIVATE_KEY: ${{ secrets.SIGNING_GPG_PRIVATE_KEY }}
PASSPHRASE: ${{ secrets.SIGNING_GPG_PASSPHRASE }}
SIGNING_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }}
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: dist/**/*
+13 -1
View File
@@ -1,4 +1,16 @@
CHANGELOG.md
/dist
/snapshot
.server/
.vscode/
*.fingerprint
*.tar
*.jar
*.war
*.ear
*.jpi
*.hpi
*.zip
.idea/
*.log
.images
@@ -16,4 +28,4 @@ coverage.txt
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
*.out
+8 -3
View File
@@ -1,4 +1,9 @@
linter-settings:
# TODO: enable this when we have coverage on docstring comments
#issues:
# # The list of ids of default excludes to include or disable.
# include:
# - EXC0002 # disable excluding of issues about comments from golint
linters:
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
disable-all: true
@@ -23,11 +28,9 @@ linters:
- gosimple
- govet
- ineffassign
- maligned
- misspell
- nakedret
- nolintlint
- prealloc
- rowserrcheck
- scopelint
- staticcheck
@@ -49,6 +52,8 @@ linters:
# - 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 # this unnecessarily requires preallocations for slices
# - testpackage
# - wsl
+56
View File
@@ -0,0 +1,56 @@
release:
# If set to auto, will mark the release as not ready for production in case there is an indicator for this in the
# tag e.g. v1.0.0-rc1 .If set to true, will mark the release as not ready for production.
prerelease: false
# If set to true, will not auto-publish the release. This is done to allow us to review the changelog before publishing.
draft: true
signs:
- artifacts: checksum
args: ["--output", "${signature}", "--detach-sign", "${artifact}"]
builds:
- binary: syft
env:
- CGO_ENABLED=0
goos:
# windows not supported yet (due to jotframe)
# - windows
- linux
- darwin
goarch:
- amd64
# 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}}
nfpms:
- license: "Apache 2.0"
maintainer: "Anchore, Inc"
homepage: &website "https://github.com/anchore/syft"
description: &description "A tool that generates a Software Bill Of Materials (SBOM) from container images and filesystems"
formats:
- rpm
- deb
brews:
- tap:
owner: anchore
name: homebrew-syft
homepage: *website
description: *description
archives:
- format: tar.gz
format_overrides:
- goos: windows
format: zip
+103
View File
@@ -0,0 +1,103 @@
# Contributing to `syft`
If you are looking to contribute to this project and want to open a Github pull request ("PR"), there are a few guidelines of what we are looking for in patches. Make sure you go through this document and ensure that your code proposal is aligned.
## Sign your work
The `sign-off` is an added line at the end of the explanation for the commit, certifying that you wrote it or otherwise have the right to submit it as an open-source patch. By submitting a contribution, you agree to be bound by the terms of the DCO Version 1.1 and Apache License Version 2.0.
Signing a commit certifies the below Developer's Certificate of Origin (DCO):
```text
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
```
All contributions to this project are licensed under the [Apache License Version 2.0, January 2004](http://www.apache.org/licenses/).
When committing your change, you can add the required line manually so that it looks like this:
```text
Signed-off-by: John Doe <john.doe@example.com>
```
Alternatively, configure your Git client with your name and email to use the `-s` flag when creating a commit:
```text
$ git config --global user.name "John Doe"
$ git config --global user.email "john.doe@example.com"
```
Creating a signed commit is then possible with `-s`:
```text
$ git commit -s -m "this is a commit message"
```
To double check if the commit was signed, look at the log output:
```text
$ git log -1
commit 37ceh170e4hb283bb73d958f2036ee5k07e7fde7 (HEAD -> issue-35, origin/main, main)
Author: John Doe <john.doe@example.com>
Date: Mon Aug 1 11:27:13 2020 -0400
this is a commit message
Signed-off-by: John Doe <john.doe@example.com>
```
[//]: # (TODO: Commit guidelines, granular commits)
[//]: # (TODO: Commit guidelines, descriptive messages)
[//]: # (TODO: Commit guidelines, commit title, extra body description)
[//]: # (TODO: PR title and description)
## Test your changes
This project has a `Makefile` which includes many helpers running both unit and integration tests. Although PRs will have automatic checks for these, it is useful to run them locally, ensuring they pass before submitting changes. Ensure you've bootstrapped once before running tests:
```text
$ make bootstrap
```
You only need to bootstrap once. After the bootstrap process, you can run the tests as many times as needed:
```text
$ make unit
$ make integration
```
## Document your changes
When proposed changes are modifying user-facing functionality or output, it is expected the PR will include updates to the documentation as well.
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+259 -38
View File
@@ -1,58 +1,279 @@
BIN = syft
TEMPDIR = ./.tmp
RESULTSDIR = $(TEMPDIR)/results
COVER_REPORT = $(RESULTSDIR)/cover.report
COVER_TOTAL = $(RESULTSDIR)/cover.total
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false --config .golangci.yaml
BOLD := $(shell tput bold)
PURPLE := $(shell tput setaf 5)
GREEN := $(shell tput setaf 2)
RESET := $(shell tput sgr0)
ACC_TEST_IMAGE = centos:8.2.2004
ACC_DIR = ./test/acceptance
BOLD := $(shell tput -T linux bold)
PURPLE := $(shell tput -T linux setaf 5)
GREEN := $(shell tput -T linux setaf 2)
CYAN := $(shell tput -T linux setaf 6)
RED := $(shell tput -T linux setaf 1)
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 := 72
ifndef TEMPDIR
$(error TEMPDIR is not set)
## Build variables
DISTDIR=./dist
SNAPSHOTDIR=./snapshot
GITTREESTATE=$(if $(shell git status --porcelain),dirty,clean)
SNAPSHOT_CMD=$(shell realpath $(shell pwd)/$(SNAPSHOTDIR)/syft_linux_amd64/syft)
ifeq "$(strip $(VERSION))" ""
override VERSION = $(shell git describe --always --tags --dirty)
endif
.PHONY: all boostrap lint lint-fix unit coverage integration build-release
## Variable assertions
all: lint unit integration
ifndef TEMPDIR
$(error TEMPDIR is not set)
endif
ifndef RESULTSDIR
$(error RESULTSDIR is not set)
endif
ifndef ACC_DIR
$(error ACC_DIR is not set)
endif
ifndef DISTDIR
$(error DISTDIR is not set)
endif
ifndef SNAPSHOTDIR
$(error SNAPSHOTDIR is not set)
endif
define title
@printf '$(TITLE)$(1)$(RESET)\n'
endef
## Tasks
.PHONY: all
all: clean static-analysis test ## Run all linux-based checks (linting, license check, unit, integration, and linux acceptance tests)
@printf '$(SUCCESS)All checks pass!$(RESET)\n'
bootstrap:
@printf '$(TITLE)Downloading dependencies$(RESET)\n'
# install project dependencies
go get ./...
mkdir -p $(TEMPDIR)
# install golangci-lint
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b .tmp/ v1.26.0
# install go-acc
GOPATH=$(shell realpath ${TEMPDIR}) GO111MODULE=off go get github.com/ory/go-acc
# cleanup
rm -rf $(TEMPDIR)/src
.PHONY: test
test: unit validate-cyclonedx-schema integration acceptance-linux ## Run all tests (currently unit, integration, and linux acceptance tests)
lint:
@printf '$(TITLE)Running linters$(RESET)\n'
test -z "$(shell gofmt -l -s .)"
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}'
.PHONY: ci-bootstrap
ci-bootstrap: bootstrap
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y bc jq libxml2-utils
.PHONY: bootstrap
bootstrap: ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
$(call title,Bootstrapping dependencies)
@pwd
# prep temp dirs
mkdir -p $(TEMPDIR)
mkdir -p $(RESULTSDIR)
# install go dependencies
go mod download
# install utilities
[ -f "$(TEMPDIR)/golangci" ] || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.26.0
[ -f "$(TEMPDIR)/bouncer" ] || curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
[ -f "$(TEMPDIR)/goreleaser" ] || curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.140.0
.PHONY: static-analysis
static-analysis: lint check-licenses
.PHONY: lint
lint: ## Run gofmt + golangci lint checks
$(call title,Running linters)
# ensure there are no go fmt differences
@printf "files with gofmt issues: [$(shell gofmt -l -s .)]\n"
@test -z "$(shell gofmt -l -s .)"
# run all golangci-lint rules
$(LINTCMD)
lint-fix:
@printf '$(TITLE)Running lint fixers$(RESET)\n'
# go tooling does not play well with certain filename characters, ensure the common cases don't result in future "go get" failures
$(eval MALFORMED_FILENAMES := $(shell find . | grep -e ':'))
@bash -c "[[ '$(MALFORMED_FILENAMES)' == '' ]] || (printf '\nfound unsupported filename characters:\n$(MALFORMED_FILENAMES)\n\n' && false)"
.PHONY: lint-fix
lint-fix: ## Auto-format all source code + run golangci lint fixers
$(call title,Running lint fixers)
gofmt -w -s .
$(LINTCMD) --fix
unit:
@printf '$(TITLE)Running unit tests$(RESET)\n'
go test --race ./...
.PHONY: check-licenses
check-licenses:
$(TEMPDIR)/bouncer check
coverage:
@printf '$(TITLE)Running unit tests + coverage$(RESET)\n'
$(TEMPDIR)/bin/go-acc -o $(TEMPDIR)/coverage.txt ./...
.PHONY: validate-cyclonedx-schema
validate-cyclonedx-schema:
cd schema/cyclonedx && make
# TODO: add benchmarks
.PHONY: unit
unit: fixtures ## Run unit tests (with coverage)
$(call title,Running unit tests)
go test -coverprofile $(COVER_REPORT) ./...
@go tool cover -func $(COVER_REPORT) | grep total | awk '{print substr($$3, 1, length($$3)-1)}' > $(COVER_TOTAL)
@echo "Coverage: $$(cat $(COVER_TOTAL))"
@if [ $$(echo "$$(cat $(COVER_TOTAL)) >= $(COVERAGE_THRESHOLD)" | bc -l) -ne 1 ]; then echo "$(RED)$(BOLD)Failed coverage quality gate (> $(COVERAGE_THRESHOLD)%)$(RESET)" && false; fi
integration:
@printf '$(TITLE)Running integration tests...$(RESET)\n'
go test -tags=integration ./integration
.PHONY: integration
integration: ## Run integration tests
$(call title,Running integration tests)
go test -v -tags=integration ./test/integration
build-release:
go build -s -w -X main.version="$(git describe --tags --dirty --always)" \
-X main.commit="$(git describe --dirty --always)" \
-X main.buildTime="$(date --rfc-3339=seconds --utc)"
# note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted
integration-fingerprint:
find test/integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/integration/test-fixtures/tar-cache.fingerprint
.PHONY: java-packages-fingerprint
java-packages-fingerprint:
@cd syft/cataloger/java/test-fixtures/java-builds && \
make packages.fingerprint
.PHONY: fixtures
fixtures:
$(call title,Generating test fixtures)
cd syft/cataloger/java/test-fixtures/java-builds && make
.PHONY: generate-json-schema
generate-json-schema: clean-json-schema-examples integration ## Generate a new json schema for the json presenter, derived from integration test cases
docker run \
-i \
--rm \
-v $(shell pwd)/schema/json:/work \
-w /work \
python:3.8 \
bash -x -c "\
pip install -r requirements.txt && \
python generate.py \
"
.PHONY: clear-test-cache
clear-test-cache: ## Delete all test cache (built docker image tars)
find . -type f -wholename "**/test-fixtures/tar-cache/*.tar" -delete
.PHONY: check-pipeline
check-pipeline: ## Run local CircleCI pipeline locally (sanity check)
$(call title,Check pipeline)
# note: this is meant for local development & testing of the pipeline, NOT to be run in CI
mkdir -p $(TEMPDIR)
circleci config process .circleci/config.yml > .tmp/circleci.yml
circleci local execute -c .tmp/circleci.yml --job "Static Analysis"
circleci local execute -c .tmp/circleci.yml --job "Unit & Integration Tests (go-latest)"
@printf '$(SUCCESS)Pipeline checks pass!$(RESET)\n'
proto:
protoc -I syft/plugin/proto/ syft/plugin/proto/cataloger.proto --go_out=plugins=grpc:syft/plugin/proto/
.PHONY: build
build: $(SNAPSHOTDIR) ## Build release snapshot binaries and packages
$(SNAPSHOTDIR): ## Build snapshot release binaries and packages
$(call title,Building snapshot artifacts)
# create a config with the dist dir overridden
echo "dist: $(SNAPSHOTDIR)" > $(TEMPDIR)/goreleaser.yaml
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
# build release snapshots
BUILD_GIT_TREE_STATE=$(GITTREESTATE) \
$(TEMPDIR)/goreleaser release --skip-publish --rm-dist --snapshot --config $(TEMPDIR)/goreleaser.yaml
.PHONY: acceptance-mac
acceptance-mac: $(SNAPSHOTDIR) ## Run acceptance tests on build snapshot binaries and packages (Mac)
$(call title,Running acceptance test: Run on Mac)
$(ACC_DIR)/mac.sh \
$(SNAPSHOTDIR) \
$(ACC_DIR) \
$(ACC_TEST_IMAGE) \
$(RESULTSDIR)
.PHONY: acceptance-linux
acceptance-linux: acceptance-test-deb-package-install acceptance-test-rpm-package-install ## Run acceptance tests on build snapshot binaries and packages (Linux)
# note: this is used by CI to determine if the inline-scan report cache should be busted for the inline-compare tests
.PHONY: compare-fingerprint
compare-fingerprint:
find test/inline-compare/* -type f -exec md5sum {} + | grep -v '\-reports' | grep -v 'fingerprint' | awk '{print $1}' | sort | md5sum | tee test/inline-compare/inline-compare.fingerprint
.PHONY: compare-snapshot
compare-snapshot: $(SNAPSHOTDIR) ## Compare the reports of a run of a snapshot build of syft against inline-scan
chmod 755 $(SNAPSHOT_CMD)
@cd test/inline-compare && SYFT_CMD=$(SNAPSHOT_CMD) make
.PHONY: compare
compare: ## Compare the reports of a run of a main-branch build of syft against inline-scan
@cd test/inline-compare && make
.PHONY: acceptance-test-deb-package-install
acceptance-test-deb-package-install: $(SNAPSHOTDIR)
$(call title,Running acceptance test: DEB install)
$(ACC_DIR)/deb.sh \
$(SNAPSHOTDIR) \
$(ACC_DIR) \
$(ACC_TEST_IMAGE) \
$(RESULTSDIR)
.PHONY: acceptance-test-rpm-package-install
acceptance-test-rpm-package-install: $(SNAPSHOTDIR)
$(call title,Running acceptance test: RPM install)
$(ACC_DIR)/rpm.sh \
$(SNAPSHOTDIR) \
$(ACC_DIR) \
$(ACC_TEST_IMAGE) \
$(RESULTSDIR)
.PHONY: changlog
changelog:
mkdir -p $(DISTDIR)
@docker run -it --rm \
-v "$(shell pwd)":/usr/local/src/your-app ferrarimarco/github-changelog-generator \
--user anchore \
--project $(BIN) \
-t ${GITHUB_TOKEN} \
--no-pr-wo-labels \
--no-issues-wo-labels \
--unreleased-only \
--future-release $(VERSION)
.PHONY: release
release: clean-dist changelog ## Build and publish final binaries and packages
$(call title,Publishing release artifacts)
# create a config with the dist dir overridden
echo "dist: $(DISTDIR)" > $(TEMPDIR)/goreleaser.yaml
cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml
# release
BUILD_GIT_TREE_STATE=$(GITTREESTATE) \
$(TEMPDIR)/goreleaser \
--rm-dist \
--config $(TEMPDIR)/goreleaser.yaml \
--release-notes <(cat CHANGELOG.md)
# verify checksum signatures
.github/scripts/verify-signature.sh "$(DISTDIR)"
# upload the version file that supports the application version update check (excluding pre-releases)
.github/scripts/update-version-file.sh "$(DISTDIR)" "$(VERSION)"
.PHONY: clean
clean: clean-dist clean-snapshot clean-json-schema-examples ## Remove previous builds and result reports
rm -rf $(RESULTSDIR)/*
.PHONY: clean-snapshot
clean-snapshot:
rm -rf $(SNAPSHOTDIR) $(TEMPDIR)/goreleaser.yaml
.PHONY: clean-dist
clean-dist:
rm -rf $(DISTDIR) $(TEMPDIR)/goreleaser.yaml
.PHONY: clean-json-schema-examples
clean-json-schema-examples:
rm -f schema/json/examples/*
+109 -1
View File
@@ -1 +1,109 @@
# imgbom (TBD Name)
# syft
[![CircleCI](https://circleci.com/gh/anchore/syft.svg?style=svg)](https://circleci.com/gh/anchore/syft)
[![Go Report Card](https://goreportcard.com/badge/github.com/anchore/syft)](https://goreportcard.com/report/github.com/anchore/syft)
[![GitHub release](https://img.shields.io/github/release/anchore/syft.svg)](https://github.com/anchore/syft/releases/latest)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/anchore/syft/blob/main/LICENSE)
A CLI tool and go library for generating a Software Bill of Materials (SBOM) from container images and filesystems.
![syft-demo](https://user-images.githubusercontent.com/590471/90277200-2a253000-de33-11ea-893f-32c219eea11a.gif)
**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)
> :warning: **This is pre-release software** and it may not work as expected. If you encounter an issue, please [let us know using the issue tracker](https://github.com/anchore/syft/issues).
## Getting started
To generate an SBOM for an image:
```
syft <image>
```
The above output includes only software that is visible in the container (i.e., the squashed representation of the image).
To include software from all image layers in the SBOM, regardless of its presence in the final image, provide `--scope all-layers`:
```
syft <image> --scope all-layers
```
Syft can generate a SBOM from a variety of sources:
```
# catalog a docker image tar (from the result of "docker image save ... -o image.tar" command)
syft docker-archive://path/to/image.tar
# catalog a directory
syft dir://path/to/dir
```
By default Syft shows a summary table, however, more detailed `text` and `json` formats are also available.
```
syft <image> -o json
syft <image> -o text
```
## Installation
**Recommended**
```bash
# install the latest version to /usr/local/bin
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# install a specific version into a specific dir
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s <RELEASE_VERSION> -b <SOME_BIN_PATH>
```
**macOS**
```bash
brew tap anchore/syft
brew install syft
```
You may experience a "macOS cannot verify app is free from malware" error upon running Syft because it is not yet signed and notarized. You can override this using `xattr`.
```bash
xattr -rd com.apple.quarantine syft
```
## Configuration
Configuration search paths:
- `.syft.yaml`
- `.syft/config.yaml`
- `~/.syft.yaml`
- `<XDG_CONFIG_HOME>/syft/config.yaml`
Configuration options (example values are the default):
```yaml
# same as -o ; the output format of the SBOM report (options: table, text, json)
output: "table"
# same as -s ; the search space to look for packages (options: all-layers, squashed)
scope: "squashed"
# same as -q ; suppress all output (except for the SBOM report)
quiet: false
log:
# use structured logging
structured: false
# the log level; note: detailed logging suppress the ETUI
level: "error"
# location to write the log file (default is not to have a log file)
file: ""
# enable/disable checking for application updates on startup
check-for-app-update: true
```
## Future plans
The following areas of potential development are currently being investigated:
- Add CycloneDX to list of output formats
- Establish a stable interchange format w/Grype
+99
View File
@@ -0,0 +1,99 @@
# Release Process
A good release process has the following qualities:
1. There is a way to plan what should be in a release
1. There is a way to see what is actually in a release
1. Allow for different kinds of releases (major breaking vs backwards compatible
enhancements vs patch updates)
1. Specify a repeatable way to build and publish software artifacts
## Planning a release
To indicate a set of features to be released together add each issue to an in-repository
Milestone named with major-minor version to be released (e.g. `v0.1`). It is OK for other
features to be in the release that were not originally planned, and these issues and PRs
do not need to be added to the Milestone in question. Only the set of features that, when
completed, would allow the release to be considered complete. A Milestone is only used to:
- Plan what is desired to be in a release
- Track progress to indicate when we may be ready to cut a new release
Not all releases need to be planned. For instance, patch releases for fixes should be
released when they are ready and when releasing would not interfere with another current
release (where some partial or breaking features have already been merged). Beta releases
and release candidates should not be independently planned from the non-beta release. That
is, the features for a `v0.1-beta.1` release should be planned under the `v0.1` Milestone,
not under a separate `v0.1-beta.1` Milestone.
Unless necessary, feature releases should be small and frequent, which may obviate the
need for regular release planning under a Milestone.
## What is in a release
Milestones are specifically for planning a release, not necessarily tracking all changes
that a release may bring (and more importantly, not all releases are necessarily planned
either).
This is one of the (many) reasons for a Changelog. A good Changelog lists changes grouped
by the type of change (new, enhancement, deprecation, breaking, bug fix, security fix), in
chronological order (within groups), linking the PR where the change was made in the
Changelog line. Furthermore, there should be a place to see all released versions, the
release date for each release, the semantic version of the release, and the set of changes
for each release.
This project auto-generates the Changelog contents for each current release and posts the
generated contents to the GitHub Release page. Leveraging the GitHub Releases feature
allows GitHub to manage the Changelog on each release outside of the git repository while
still being hosted with the released assets.
The Changelog is generated from the metadata from in-repository issues and PRs, using
labels to guide what kind of change each item is (e.g. breaking, new feature, bug fix,
etx). Only issues/PRs with select labels are included in the Changelog, and only if the
issue/PR was created after the last release. Additional labels are used to exclude items
from the Changelog.
The above suggestions imply that we should:
- Ensure there is a sufficient title for each PR and issue title to be included in the
Changelog
- The appropriate label is applied to PRs and/or issues to drive specific change type
sections (deprecated, breaking, security, bug, etc)
With this approach as we cultivate good organization of PRs and issues we automatically
get an equally good Changelog.
## Major, minor, and patch releases
The latest version of the tool is the only supported version, which implies that multiple
parallel release branches will not be a regular process (if ever). Multiple releases can
be planned in parallel, however, only one can be actively developed at a time. That is, if
PRs attached to a release Milestone have been merged into the main branch, that release is
now the "next" release. This implies that the source of truth for release lies with the
git log and Changelog, not with the release Milestones (which are purely for planning and
tracking).
Semantic versioning should be used to indicate breaking changes, new features, and fixes.
The exception to this is `< 1.0`, where the major version is not bumped for breaking changes,
instead the minor version indicates both new features and breaking changes.
## Cutting a release
Ideally releasing should be done often with small increments when possible. Unless a
breaking change is blocking the release, or no fixes/features have been merged, a good
target release cadence is between every 2 or 4 weeks.
This release process itself should be as automated as possible, and have only a few steps:
1. Tag the main branch with a full semantic-version, prefixed with a `v`. If there is a
milestone with a partial version, the full version should be used for the git tag (e.g.
with a Milestone of `v0.1` the tag should be `v0.1.0`).
1. Push the tag, the release pipeline will generate and publish all assets as well as a
draft GitHub Release.
1. Promote the GitHub Release from draft to public. Note: since extra assets are made
available immediately from previous steps (i.e. the brew formula) the release should
only be in this state for a small amount of time (minutes).
1. If there is a release Milestone, close it.
-50
View File
@@ -1,50 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/anchore/imgbom/imgbom/presenter"
"github.com/anchore/imgbom/imgbom/scope"
"github.com/anchore/imgbom/internal/config"
"github.com/spf13/viper"
)
var cliOpts = config.CliOnlyOptions{}
func setCliOptions() {
rootCmd.PersistentFlags().StringVarP(&cliOpts.ConfigPath, "config", "c", "", "application config file")
// scan options
flag := "scope"
rootCmd.Flags().StringP(
"scope", "s", scope.AllLayersScope.String(),
fmt.Sprintf("selection of layers to analyze, options=%v", scope.Options))
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
// output & formatting options
flag = "output"
rootCmd.Flags().StringP(
flag, "o", presenter.JSONPresenter.String(),
fmt.Sprintf("report output formatter, options=%v", presenter.Options),
)
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
flag = "quiet"
rootCmd.Flags().BoolP(
flag, "q", false,
"suppress all logging output",
)
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
rootCmd.Flags().CountVarP(&cliOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
}
+140
View File
@@ -0,0 +1,140 @@
package cmd
import (
"fmt"
"os"
"path"
"github.com/adrg/xdg"
"github.com/anchore/syft/internal"
"github.com/spf13/cobra"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/scope"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/format"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/logger"
"github.com/anchore/syft/syft"
"github.com/spf13/viper"
"github.com/wagoodman/go-partybus"
"gopkg.in/yaml.v2"
)
var appConfig *config.Application
var eventBus *partybus.Bus
var eventSubscription *partybus.Subscription
var cliOpts = config.CliOnlyOptions{}
func init() {
setGlobalCliOptions()
cobra.OnInitialize(
initAppConfig,
initLogging,
logAppConfig,
initEventBus,
)
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
os.Exit(1)
}
}
func setGlobalCliOptions() {
rootCmd.PersistentFlags().StringVarP(&cliOpts.ConfigPath, "config", "c", "", "application config file")
// scan options
flag := "scope"
rootCmd.Flags().StringP(
"scope", "s", scope.SquashedScope.String(),
fmt.Sprintf("selection of layers to catalog, options=%v", scope.Options))
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
// output & formatting options
flag = "output"
rootCmd.Flags().StringP(
flag, "o", string(presenter.TablePresenter),
fmt.Sprintf("report output formatter, options=%v", presenter.Options),
)
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
flag = "quiet"
rootCmd.Flags().BoolP(
flag, "q", false,
"suppress all logging output",
)
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
rootCmd.Flags().CountVarP(&cliOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
// plugins
defaultPluginDir := path.Join(xdg.CacheHome, internal.ApplicationName, "plugins")
flag = "plugins.dir"
rootCmd.Flags().String(
flag, defaultPluginDir,
"plugin storage directory",
)
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
}
func initAppConfig() {
cfg, err := config.LoadConfigFromFile(viper.GetViper(), &cliOpts)
if err != nil {
fmt.Printf("failed to load application config: \n\t%+v\n", err)
os.Exit(1)
}
appConfig = cfg
}
func initLogging() {
cfg := logger.LogrusConfig{
EnableConsole: (appConfig.Log.FileLocation == "" || appConfig.CliOptions.Verbosity > 0) && !appConfig.Quiet,
EnableFile: appConfig.Log.FileLocation != "",
Level: appConfig.Log.LevelOpt,
Structured: appConfig.Log.Structured,
FileLocation: appConfig.Log.FileLocation,
}
logWrapper := logger.NewLogrusLogger(cfg)
syft.SetLogger(logWrapper)
stereoscope.SetLogger(&logger.LogrusNestedLogger{
Logger: logWrapper.Logger.WithField("from-lib", "stereoscope"),
})
}
func logAppConfig() {
appCfgStr, err := yaml.Marshal(&appConfig)
if err != nil {
log.Debugf("Could not display application config: %+v", err)
} else {
log.Debugf("Application config:\n%+v", format.Magenta.Format(string(appCfgStr)))
}
}
func initEventBus() {
eventBus = partybus.NewBus()
eventSubscription = eventBus.Subscribe()
stereoscope.SetBus(eventBus)
syft.SetBus(eventBus)
}
-52
View File
@@ -1,52 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/anchore/imgbom/imgbom"
"github.com/anchore/imgbom/internal/config"
"github.com/anchore/imgbom/internal/format"
"github.com/anchore/imgbom/internal/logger"
"github.com/anchore/stereoscope"
"github.com/spf13/viper"
"go.uber.org/zap"
"gopkg.in/yaml.v2"
)
var appConfig *config.Application
var log *zap.SugaredLogger
func initAppConfig() {
cfg, err := config.LoadConfigFromFile(viper.GetViper(), &cliOpts)
if err != nil {
fmt.Printf("failed to load application config: \n\t%+v\n", err)
os.Exit(1)
}
appConfig = cfg
}
func initLogging() {
config := logger.LogConfig{
EnableConsole: (appConfig.Log.FileLocation == "" || appConfig.CliOptions.Verbosity > 0) && !appConfig.Quiet,
EnableFile: appConfig.Log.FileLocation != "",
Level: appConfig.Log.LevelOpt,
Structured: appConfig.Log.Structured,
FileLocation: appConfig.Log.FileLocation,
}
logWrapper := logger.NewZapLogger(config)
log = logWrapper.Logger
imgbom.SetLogger(logWrapper)
stereoscope.SetLogger(logWrapper)
}
func logAppConfig() {
appCfgStr, err := yaml.Marshal(&appConfig)
if err != nil {
log.Debugf("Could not display application config: %+v", err)
} else {
log.Debugf("Application config:\n%+v", format.Magenta.Format(string(appCfgStr)))
}
}
+77 -76
View File
@@ -3,97 +3,98 @@ package cmd
import (
"fmt"
"os"
"strconv"
"github.com/anchore/imgbom/imgbom"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/internal"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/presenter"
"github.com/spf13/cobra"
"github.com/wagoodman/go-partybus"
)
var rootCmd = &cobra.Command{
Use: fmt.Sprintf("%s [IMAGE]", internal.ApplicationName),
Short: "A container image BOM tool", // TODO: add copy
Long: internal.Tprintf(`\
Use: fmt.Sprintf("%s [SOURCE]", internal.ApplicationName),
Short: "A tool for generating a Software Bill Of Materials (SBOM) from container images and filesystems",
Long: internal.Tprintf(`
Supports the following image sources:
{{.appName}} yourrepo/yourimage:tag defaults to using images from a docker daemon
{{.appName}} docker://yourrepo/yourimage:tag explicitly use the docker daemon
{{.appName}} tar://path/to/yourimage.tar use a tarball from disk
{{.appName}} dir://path/to/yourproject read directly from a path in disk
`, map[string]interface{}{
"appName": internal.ApplicationName,
}),
Args: cobra.MaximumNArgs(1),
Run: runCmdWrapper,
}
func init() {
setCliOptions()
cobra.OnInitialize(initAppConfig)
cobra.OnInitialize(initLogging)
cobra.OnInitialize(logAppConfig)
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Errorf("could not start application: %w", err)
os.Exit(1)
}
}
func runCmdWrapper(cmd *cobra.Command, args []string) {
os.Exit(doRunCmd(cmd, args))
}
func doRunCmd(cmd *cobra.Command, args []string) int {
userImageStr := args[0]
log.Infof("Fetching image '%s'", userImageStr)
img, err := stereoscope.GetImage(userImageStr)
if err != nil {
log.Errorf("could not fetch image '%s': %w", userImageStr, err)
return 1
}
defer stereoscope.Cleanup()
log.Info("Cataloging image")
catalog, err := imgbom.CatalogImage(img, appConfig.ScopeOpt)
if err != nil {
log.Errorf("could not catalog image: %w", err)
return 1
}
log.Info("Complete!")
// err = presenter.GetPresenter(appConfig.PresenterOpt).Present(os.Stdout, img, catalog)
// if err != nil {
// log.Errorf("could not format catalog results: %w", err)
// return 1
// }
result := catalog.SearchName("libselinux")
fmt.Println(result)
if result != nil {
for _, hit := range result.Hits {
pkgId, err := strconv.Atoi(hit.ID)
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
err := cmd.Help()
if err != nil {
// TODO: just no...
panic(err)
log.Errorf(err.Error())
os.Exit(1)
}
fmt.Println(pkgId, catalog.Package(pkg.ID(pkgId)), hit.Score)
os.Exit(1)
}
}
fmt.Println("------------------------------------------")
result = catalog.SearchMetadata("libselinux")
fmt.Println(result)
if result != nil {
for _, hit := range result.Hits {
pkgId, err := strconv.Atoi(hit.ID)
if err != nil {
// TODO: just no...
panic(err)
}
fmt.Println(pkgId, catalog.Package(pkg.ID(pkgId)), hit.Score)
err := doRunCmd(cmd, args)
if err != nil {
log.Errorf(err.Error())
os.Exit(1)
}
}
return 0
},
}
func startWorker(userInput string) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
if appConfig.CheckForAppUpdate {
isAvailable, newVersion, err := version.IsUpdateAvailable()
if err != nil {
log.Errorf(err.Error())
}
if isAvailable {
log.Infof("new version of %s is available: %s", internal.ApplicationName, newVersion)
bus.Publish(partybus.Event{
Type: event.AppUpdateAvailable,
Value: newVersion,
})
} else {
log.Debugf("no new %s update available", internal.ApplicationName)
}
}
config := syft.Config{
PluginDirectory: appConfig.Plugins.Directory,
}
client, err := syft.NewClient(config)
if err != nil {
errs <- fmt.Errorf("could not initialize syft: %+v", err)
return
}
catalog, scope, distro, err := client.Catalog(userInput, appConfig.ScopeOpt)
if err != nil {
errs <- fmt.Errorf("failed to catalog input: %+v", err)
return
}
bus.Publish(partybus.Event{
Type: event.CatalogerFinished,
Value: presenter.GetPresenter(appConfig.PresenterOpt, *scope, catalog, distro),
})
}()
return errs
}
func doRunCmd(_ *cobra.Command, args []string) error {
userInput := args[0]
errs := startWorker(userInput)
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
return ux(errs, eventSubscription)
}
+19 -14
View File
@@ -3,17 +3,12 @@ package cmd
import (
"fmt"
"github.com/anchore/imgbom/internal"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
"github.com/spf13/cobra"
)
type Version struct {
Version string
Commit string
BuildTime string
}
var version *Version
var showVerboseVersionInfo bool
var versionCmd = &cobra.Command{
Use: "version",
@@ -22,13 +17,23 @@ var versionCmd = &cobra.Command{
}
func init() {
versionCmd.Flags().BoolVarP(&showVerboseVersionInfo, "verbose", "v", false, "show additional version information")
rootCmd.AddCommand(versionCmd)
}
func SetVersion(v *Version) {
version = v
}
func printVersion(cmd *cobra.Command, args []string) {
fmt.Printf("%s %s\n", internal.ApplicationName, version.Version)
func printVersion(_ *cobra.Command, _ []string) {
versionInfo := version.FromBuild()
if showVerboseVersionInfo {
fmt.Println("Application: ", internal.ApplicationName)
fmt.Println("Version: ", versionInfo.Version)
fmt.Println("BuildDate: ", versionInfo.BuildDate)
fmt.Println("GitCommit: ", versionInfo.GitCommit)
fmt.Println("GitTreeState: ", versionInfo.GitTreeState)
fmt.Println("Platform: ", versionInfo.Platform)
fmt.Println("GoVersion: ", versionInfo.GoVersion)
fmt.Println("Compiler: ", versionInfo.Compiler)
} else {
fmt.Printf("%s %s\n", internal.ApplicationName, versionInfo.Version)
}
}
+33 -11
View File
@@ -1,25 +1,47 @@
module github.com/anchore/imgbom
module github.com/anchore/syft
go 1.14
require (
github.com/RoaringBitmap/roaring v0.4.23 // indirect
github.com/adrg/xdg v0.2.1
github.com/anchore/go-testutils v0.0.0-20200520222037-edc2bf1864fe
github.com/anchore/stereoscope v0.0.0-20200523232006-be5f3c18958f
github.com/blevesearch/bleve v1.0.9
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a // indirect
github.com/anchore/go-rpmdb v0.0.0-20200811175839-cbc751c28e8e
github.com/anchore/go-testutils v0.0.0-20200624184116-66aa578126db
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/stereoscope v0.0.0-20200903223322-a3806f515470
github.com/bmatcuk/doublestar v1.3.1
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe // indirect
github.com/dustin/go-humanize v1.0.0
github.com/go-test/deep v1.0.6
github.com/google/go-containerregistry v0.0.0-20200521151920-a873a21aff23 // indirect
github.com/golang/protobuf v1.4.2
github.com/google/go-containerregistry v0.1.1 // indirect
github.com/google/uuid v1.1.1
github.com/gookit/color v1.2.7
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 // indirect
github.com/hashicorp/go-hclog v0.14.1
github.com/hashicorp/go-multierror v1.1.0
github.com/hashicorp/go-plugin v1.3.0
github.com/hashicorp/go-version v1.2.0
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/mapstructure v1.3.1
github.com/olekukonko/tablewriter v0.0.4
github.com/package-url/packageurl-go v0.1.0
github.com/pelletier/go-toml v1.8.0
github.com/rogpeppe/go-internal v1.5.2
github.com/sergi/go-diff v1.1.0
github.com/sirupsen/logrus v1.6.0
github.com/spf13/cobra v1.0.0
github.com/spf13/viper v1.7.0
github.com/tinylib/msgp v1.1.2 // indirect
go.uber.org/zap v1.15.0
golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect
google.golang.org/protobuf v1.24.0 // indirect
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163
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/net v0.0.0-20200625001655-4c5254603344 // indirect
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 // indirect
golang.org/x/sys v0.0.0-20200610111108-226ff32320da // indirect
google.golang.org/genproto v0.0.0-20200615140333-fd031eab31e7 // indirect
google.golang.org/grpc v1.31.1
gopkg.in/ini.v1 v1.57.0 // indirect
gopkg.in/yaml.v2 v2.3.0
)
+657 -118
View File
File diff suppressed because it is too large Load Diff
-16
View File
@@ -1,16 +0,0 @@
package analyzer
import (
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/tree"
)
type Analyzer interface {
Name() string
// TODO: add ID / Name for analyze for uniquely identifying this analyzer type
SelectFiles([]*tree.FileTree) []file.Reference
// NOTE: one of the errors which is returned is "IterationNeeded", which indicates to the driver to
// continue with another Select/Analyze pass
Analyze(map[file.Reference]string) ([]pkg.Package, error)
}
-72
View File
@@ -1,72 +0,0 @@
package analyzer
import (
"github.com/anchore/imgbom/imgbom/analyzer/dpkg"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/imgbom/scope"
"github.com/anchore/imgbom/internal/log"
"github.com/anchore/stereoscope/pkg/file"
"github.com/hashicorp/go-multierror"
)
var controllerInstance controller
func init() {
controllerInstance = controller{
analyzers: make([]Analyzer, 0),
}
controllerInstance.add(dpkg.NewAnalyzer())
}
func Analyze(s scope.Scope) (pkg.Catalog, error) {
return controllerInstance.analyze(s)
}
type controller struct {
analyzers []Analyzer
}
func (c *controller) add(a Analyzer) {
log.Debugf("adding analyzer: %s", a.Name())
c.analyzers = append(c.analyzers, a)
}
func (c *controller) analyze(s scope.Scope) (pkg.Catalog, error) {
catalog := pkg.NewCatalog()
fileSelection := make([]file.Reference, 0)
// ask analyzers for files to extract from the image tar
for _, a := range c.analyzers {
fileSelection = append(fileSelection, a.SelectFiles(s.Trees)...)
log.Debugf("analyzer '%s' selected '%d' files", a.Name(), len(fileSelection))
}
// fetch contents for requested selection by analyzers
contents, err := s.Image.MultipleFileContentsByRef(fileSelection...)
if err != nil {
return pkg.Catalog{}, err
}
// perform analysis, accumulating errors for each failed analysis
var errs error
for _, a := range c.analyzers {
// TODO: check for multiple rounds of analyses by Iterate error
packages, err := a.Analyze(contents)
if err != nil {
errs = multierror.Append(errs, err)
continue
}
log.Debugf("analyzer '%s' discovered '%d' packages", a.Name(), len(packages))
for _, p := range packages {
catalog.Add(p)
}
}
if errs != nil {
return pkg.Catalog{}, errs
}
return catalog, nil
}
-69
View File
@@ -1,69 +0,0 @@
package dpkg
import (
"strings"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/internal/log"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/tree"
)
type Analyzer struct {
selectedFiles []file.Reference
}
func NewAnalyzer() *Analyzer {
return &Analyzer{}
}
func (a *Analyzer) Name() string {
return "dpkg-analyzer"
}
func (a *Analyzer) SelectFiles(trees []*tree.FileTree) []file.Reference {
files := make([]file.Reference, 0)
for _, tree := range trees {
// TODO: extract into function/slice/etc
file := tree.File("/var/lib/dpkg/status")
if file != nil {
files = append(files, *file)
}
}
a.selectedFiles = files
return files
}
func (a *Analyzer) Analyze(contents map[file.Reference]string) ([]pkg.Package, error) {
packages := make([]pkg.Package, 0)
for _, reference := range a.selectedFiles {
content, ok := contents[reference]
if !ok {
// TODO: test case
log.Errorf("analyzer '%s' file content missing: %+v", a.Name(), reference)
continue
}
entries, err := ParseEntries(strings.NewReader(content))
if err != nil {
// TODO: test case
log.Errorf("analyzer failed to parse entries (reference=%+v): %w", reference, err)
continue
}
for _, entry := range entries {
packages = append(packages, pkg.Package{
Name: entry.Package,
Version: entry.Version,
Type: pkg.DebPkg,
Source: []file.Reference{reference},
Metadata: entry,
})
}
}
return packages, nil
}
-145
View File
@@ -1,145 +0,0 @@
package dpkg
import (
"bufio"
"os"
"testing"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/go-test/deep"
)
func compareEntries(t *testing.T, left, right pkg.DpkgMetadata) {
t.Helper()
if diff := deep.Equal(left, right); diff != nil {
t.Error(diff)
}
}
func TestSinglePackage(t *testing.T) {
tests := []struct {
name string
expected pkg.DpkgMetadata
}{
{
name: "Test Single Package",
expected: pkg.DpkgMetadata{
Package: "apt",
Status: "install ok installed",
Priority: "required",
InstalledSize: "4064",
Maintainer: "APT Development Team <deity@lists.debian.org>",
Architecture: "amd64",
Version: "1.8.2",
ReplacesPkgs: "apt-transport-https (<< 1.5~alpha4~), apt-utils (<< 1.3~exp2~)",
ProvidesPkgs: "apt-transport-https (= 1.8.2)",
DependsPkgs: "adduser, gpgv | gpgv2 | gpgv1, debian-archive-keyring, libapt-pkg5.0 (>= 1.7.0~alpha3~), libc6 (>= 2.15), libgcc1 (>= 1:3.0), libgnutls30 (>= 3.6.6), libseccomp2 (>= 1.0.1), libstdc++6 (>= 5.2)",
RecommendsPkgs: "ca-certificates",
SuggestsPkgs: "apt-doc, aptitude | synaptic | wajig, dpkg-dev (>= 1.17.2), gnupg | gnupg2 | gnupg1, powermgmt-base",
ConfigFiles: `
/etc/apt/apt.conf.d/01autoremove 76120d358bc9037bb6358e737b3050b5
/etc/cron.daily/apt-compat 49e9b2cfa17849700d4db735d04244f3
/etc/kernel/postinst.d/apt-auto-removal 4ad976a68f045517cf4696cec7b8aa3a
/etc/logrotate.d/apt 179f2ed4f85cbaca12fa3d69c2a4a1c3`,
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
file, err := os.Open("test-fixtures/single")
if err != nil {
t.Fatal("Unable to read test_fixtures/single: ", err)
}
defer func() {
err := file.Close()
if err != nil {
t.Fatal("closing file failed:", err)
}
}()
reader := bufio.NewReader(file)
entry, err := parseEntry(reader)
if err != nil {
t.Fatal("Unable to read file contents: ", err)
}
compareEntries(t, entry, test.expected)
})
}
}
func TestMultiplePackages(t *testing.T) {
tests := []struct {
name string
expected []pkg.DpkgMetadata
}{
{
name: "Test Multiple Package",
expected: []pkg.DpkgMetadata{
{
Package: "tzdata",
Status: "install ok installed",
Priority: "required",
InstalledSize: "3036",
Maintainer: "GNU Libc Maintainers <debian-glibc@lists.debian.org>",
Architecture: "all",
Version: "2020a-0+deb10u1",
ReplacesPkgs: "libc0.1, libc0.3, libc6, libc6.1",
ProvidesPkgs: "tzdata-buster",
DependsPkgs: "debconf (>= 0.5) | debconf-2.0",
},
{
Package: "util-linux",
Status: "install ok installed",
Priority: "required",
InstalledSize: "4327",
Maintainer: "LaMont Jones <lamont@debian.org>",
Architecture: "amd64",
Version: "2.33.1-0.1",
ReplacesPkgs: "bash-completion (<< 1:2.8), initscripts (<< 2.88dsf-59.2~), login (<< 1:4.5-1.1~), mount (<< 2.29.2-3~), s390-tools (<< 2.2.0-1~), setpriv (<< 2.32.1-0.2~), sysvinit-utils (<< 2.88dsf-59.1~)",
DependsPkgs: "fdisk, login (>= 1:4.5-1.1~)",
SuggestsPkgs: "dosfstools, kbd | console-tools, util-linux-locales",
ConfigFiles: `
/etc/default/hwclock 3916544450533eca69131f894db0ca12
/etc/init.d/hwclock.sh 1ca5c0743fa797ffa364db95bb8d8d8e
/etc/pam.d/runuser b8b44b045259525e0fae9e38fdb2aeeb
/etc/pam.d/runuser-l 2106ea05877e8913f34b2c77fa02be45
/etc/pam.d/su ce6dcfda3b190a27a455bb38a45ff34a
/etc/pam.d/su-l 756fef5687fecc0d986e5951427b0c4f`,
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
file, err := os.Open("test-fixtures/multiple")
if err != nil {
t.Fatal("Unable to read: ", err)
}
defer func() {
err := file.Close()
if err != nil {
t.Fatal("closing file failed:", err)
}
}()
entries, err := ParseEntries(file)
if err != nil {
t.Fatal("Unable to read file contents: ", err)
}
if len(entries) != 2 {
t.Fatalf("unexpected number of entries: %d", len(entries))
}
for idx, entry := range entries {
compareEntries(t, entry, test.expected[idx])
}
})
}
}
-20
View File
@@ -1,20 +0,0 @@
package imgbom
import (
"github.com/anchore/imgbom/imgbom/analyzer"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/imgbom/scope"
"github.com/anchore/stereoscope/pkg/image"
)
// TODO: add os detection results as return value
func CatalogImage(img *image.Image, o scope.Option) (pkg.Catalog, error) {
s, err := scope.NewScope(img, o)
if err != nil {
return pkg.Catalog{}, err
}
// TODO: add OS detection here...
return analyzer.Analyze(s)
}
-4
View File
@@ -1,4 +0,0 @@
package imgbom
// note: must be a single word, all lowercase
const LibraryName = "imgbom"
-10
View File
@@ -1,10 +0,0 @@
package imgbom
import (
"github.com/anchore/imgbom/imgbom/logger"
"github.com/anchore/imgbom/internal/log"
)
func SetLogger(logger logger.Logger) {
log.Log = logger
}
-144
View File
@@ -1,144 +0,0 @@
package pkg
import (
"fmt"
"sync"
"github.com/anchore/imgbom/internal/log"
"github.com/anchore/stereoscope/pkg/file"
"github.com/blevesearch/bleve"
"github.com/blevesearch/bleve/mapping"
)
// TODO: add reader methods (by type, id, fuzzy search, etc)
var nextPackageID int64
type Catalog struct {
byID map[ID]*Package
byType map[Type][]*Package
byFile map[file.Reference][]*Package
searchSpace mapping.IndexMapping
nameSearchIndex bleve.Index
metadataSearchIndex bleve.Index
lock sync.RWMutex
}
func NewCatalog() Catalog {
searchMap := bleve.NewIndexMapping()
nameIndex, err := bleve.NewMemOnly(searchMap)
if err != nil {
// TODO: log
panic(err)
}
metadataIndex, err := bleve.NewMemOnly(searchMap)
if err != nil {
// TODO: log
panic(err)
}
return Catalog{
byID: make(map[ID]*Package),
byType: make(map[Type][]*Package),
byFile: make(map[file.Reference][]*Package),
searchSpace: searchMap,
nameSearchIndex: nameIndex,
metadataSearchIndex: metadataIndex,
}
}
func (c *Catalog) Package(id ID) *Package {
return c.byID[id]
}
func (c *Catalog) PackagesByFile(ref file.Reference) []*Package {
return c.byFile[ref]
}
func (c *Catalog) Add(p Package) {
if p.id != 0 {
log.Errorf("package already added to catalog: %s", p)
return
}
c.lock.Lock()
defer c.lock.Unlock()
p.id = ID(nextPackageID)
nextPackageID++
// store by package ID
c.byID[p.id] = &p
// store by package type
_, ok := c.byType[p.Type]
if !ok {
c.byType[p.Type] = make([]*Package, 0)
}
c.byType[p.Type] = append(c.byType[p.Type], &p)
// store by file references
for _, s := range p.Source {
_, ok := c.byFile[s]
if !ok {
c.byFile[s] = make([]*Package, 0)
}
c.byFile[s] = append(c.byFile[s], &p)
}
// index the package findings
err := c.nameSearchIndex.Index(fmt.Sprintf("%d", p.id), p.Name)
if err != nil {
// TODO: just no...
panic(err)
}
err = c.metadataSearchIndex.Index(fmt.Sprintf("%d", p.id), p.Metadata)
if err != nil {
// TODO: just no...
panic(err)
}
}
func (c *Catalog) SearchMetadata(query string) *bleve.SearchResult {
request := bleve.NewSearchRequest(bleve.NewMatchQuery(query))
result, err := c.metadataSearchIndex.Search(request)
if err != nil {
// TODO: just no...
panic(err)
}
return result
}
func (c *Catalog) SearchName(query string) *bleve.SearchResult {
request := bleve.NewSearchRequest(bleve.NewMatchQuery(query))
result, err := c.nameSearchIndex.Search(request)
if err != nil {
// TODO: just no...
panic(err)
}
return result
}
func (c *Catalog) Enumerate(types ...Type) <-chan *Package {
channel := make(chan *Package)
go func() {
defer close(channel)
for ty, packages := range c.byType {
if len(types) != 0 {
found := false
typeCheck:
for _, t := range types {
if t == ty {
found = true
break typeCheck
}
}
if !found {
continue
}
}
for _, p := range packages {
channel <- p
}
}
}()
return channel
}
-21
View File
@@ -1,21 +0,0 @@
package pkg
// TODO: consider keeping the remaining values as an embedded map
// Available fields are described at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html
// in the --showformat section
type DpkgMetadata struct {
Package string `mapstructure:"Package"`
Architecture string `mapstructure:"Architecture"`
DependsPkgs string `mapstructure:"Depends"`
InstalledSize string `mapstructure:"Installed-Size"`
Maintainer string `mapstructure:"Maintainer"`
Priority string `mapstructure:"Priority"`
ProvidesPkgs string `mapstructure:"Provides"`
RecommendsPkgs string `mapstructure:"Recommends"`
ReplacesPkgs string `mapstructure:"Replaces"`
Status string `mapstructure:"Status"`
SuggestsPkgs string `mapstructure:"Suggests"`
Version string `mapstructure:"Version"`
ConfigFiles string `mapstructure:"Conffiles"`
Description string `mapstructure:"Description"`
}
-28
View File
@@ -1,28 +0,0 @@
package pkg
import (
"fmt"
"github.com/anchore/stereoscope/pkg/file"
)
type ID int64
// TODO: add field to trace which analyzer detected this
type Package struct {
id ID
Name string
Version string
Source []file.Reference
Licenses []string
Type Type
Metadata interface{}
}
func (p Package) ID() ID {
return p.id
}
func (p Package) String() string {
return fmt.Sprintf("Pkg(type=%s, name=%s, version=%s)", p.Type, p.Name, p.Version)
}
-34
View File
@@ -1,34 +0,0 @@
package pkg
const (
UnknownPkg Type = iota
ApkPkg
DebPkg
JavaPkg
JavaScriptPkg
PacmanPkg
PythonPkg
RpmPkg
RubyPkg
)
type Type uint
var typeStr = []string{
"UnknownPackage",
"apk",
"deb",
"java",
"node",
"pacman",
"python",
"rpm",
"ruby",
}
func (t Type) String() string {
if int(t) >= len(typeStr) {
return typeStr[0]
}
return typeStr[t]
}
-114
View File
@@ -1,114 +0,0 @@
package json
import (
"encoding/json"
"io"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/internal/log"
stereoscopeImg "github.com/anchore/stereoscope/pkg/image"
)
type Presenter struct{}
func NewPresenter() *Presenter {
return &Presenter{}
}
type document struct {
Image image `json:"image"`
Artifacts []artifact `json:"artifacts"`
}
type image struct {
Layers []layer `json:"layers"`
Size int64 `json:"size"`
Digest string `json:"digest"`
MediaType string `json:"mediaType"`
Tags []string `json:"tags"`
}
type layer struct {
MediaType string `json:"mediaType"`
Digest string `json:"digest"`
Size int64 `json:"size"`
}
type source struct {
Source string `json:"source"`
Layer int `json:"layer"`
Effects []string `json:"effects"`
}
type artifact struct {
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
Analyzer string `json:"analyzer"`
Sources []source `json:"sources"`
Metadata interface{} `json:"metadata"`
}
func (pres *Presenter) Present(output io.Writer, img *stereoscopeImg.Image, catalog pkg.Catalog) error {
tags := make([]string, len(img.Metadata.Tags))
for idx, tag := range img.Metadata.Tags {
tags[idx] = tag.String()
}
doc := document{
Image: image{
Digest: img.Metadata.Digest,
Size: img.Metadata.Size,
MediaType: string(img.Metadata.MediaType),
Tags: tags,
Layers: make([]layer, len(img.Layers)),
},
Artifacts: make([]artifact, 0),
}
// populate image...
for idx, l := range img.Layers {
doc.Image.Layers[idx] = layer{
MediaType: string(l.Metadata.MediaType),
Digest: l.Metadata.Digest,
Size: l.Metadata.Size,
}
}
// populate artifacts...
for p := range catalog.Enumerate() {
art := artifact{
Name: p.Name,
Version: p.Version,
Type: p.Type.String(),
Analyzer: "TODO", // TODO
Sources: make([]source, len(p.Source)),
Metadata: p.Metadata,
}
for idx, src := range p.Source {
fileMetadata, err := img.FileCatalog.Get(src)
if err != nil {
// TODO: test case
log.Errorf("could not get metadata from catalog (presenter=json): %+v", src)
}
srcObj := source{
Source: "",
Layer: int(fileMetadata.Source.Metadata.Index),
Effects: []string{}, // TODO
}
art.Sources[idx] = srcObj
}
doc.Artifacts = append(doc.Artifacts, art)
}
bytes, err := json.Marshal(&doc)
if err != nil {
log.Errorf("failed to marshal json (presenter=json): %w", err)
}
_, err = output.Write(bytes)
return err
}
-90
View File
@@ -1,90 +0,0 @@
package json
import (
"bytes"
"flag"
"testing"
"github.com/anchore/go-testutils"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/stereoscope/pkg/file"
"github.com/sergi/go-diff/diffmatchpatch"
)
var update = flag.Bool("update", false, "update the *.golden files for json presenters")
// TODO: add a JSON schema and write a test that validates output against the schema
// func validateAgainstV1Schema(t *testing.T, json string) {
// fullSchemaPath, err := filepath.Abs("v1-schema.json")
// if err != nil {
// t.Fatal("could not get path to schema:", err)
// }
// schemaLoader := gojsonschema.NewReferenceLoader(fmt.Sprintf("file://%s", fullSchemaPath))
// documentLoader := gojsonschema.NewStringLoader(json)
// result, err := gojsonschema.Validate(schemaLoader, documentLoader)
// if err != nil {
// t.Fatal("unable to validate json schema:", err.Error())
// }
// if !result.Valid() {
// t.Errorf("failed json schema validation:")
// for _, desc := range result.Errors() {
// t.Errorf(" - %s\n", desc)
// }
// }
// }
func TestJsonPresenter(t *testing.T) {
pres := NewPresenter()
var buffer bytes.Buffer
testImage := "image-simple"
if *update {
testutils.UpdateGoldenFixtureImage(t, testImage)
}
catalog := pkg.NewCatalog()
img := testutils.GetGoldenFixtureImage(t, testImage)
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package-1",
Version: "1.0.1",
Source: []file.Reference{
*img.SquashedTree.File("/somefile-1.txt"),
},
Type: pkg.DebPkg,
})
catalog.Add(pkg.Package{
Name: "package-2",
Version: "2.0.1",
Source: []file.Reference{
*img.SquashedTree.File("/somefile-2.txt"),
},
Type: pkg.DebPkg,
})
// run presenter
err := pres.Present(&buffer, img, catalog)
if err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *update {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
// TODO: add me back in when there is a JSON schema
// validateAgainstV1Schema(t, string(actual))
}
@@ -1 +0,0 @@
{"image":{"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:056c0789fa9ad629ceae6d09713fb035f84115af3c4a88a43aa60f13bc683053","size":22},{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:b461c48116592c570a66fed71d5b09662a8172e168b7938cf317af47872cdc9b","size":16},{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:00b80053e05c01da485015610d288ce3185fac00d251e2ada02b45a7a7c5f589","size":27}],"size":65,"digest":"sha256:3c53d2d891940f8d8e95acb77b58752f54dc5de9d91d19dd90ced2db76256cea","mediaType":"application/vnd.docker.distribution.manifest.v2+json","tags":["anchore-fixture-image-simple:04e16e44161c8888a1a963720fd0443cbf7eef8101434c431de8725cd98cc9f7"]},"artifacts":[{"name":"package-1","version":"1.0.1","type":"deb","analyzer":"TODO","sources":[{"source":"","layer":0,"effects":[]}],"metadata":null},{"name":"package-2","version":"2.0.1","type":"deb","analyzer":"TODO","sources":[{"source":"","layer":1,"effects":[]}],"metadata":null}]}
-36
View File
@@ -1,36 +0,0 @@
package presenter
import "strings"
const (
UnknownPresenter Option = iota
JSONPresenter
)
var optionStr = []string{
"UnknownPresenter",
"json",
}
var Options = []Option{
JSONPresenter,
}
type Option uint
func ParseOption(userStr string) Option {
switch strings.ToLower(userStr) {
case strings.ToLower(JSONPresenter.String()):
return JSONPresenter
default:
return UnknownPresenter
}
}
func (o Option) String() string {
if int(o) >= len(optionStr) {
return optionStr[0]
}
return optionStr[o]
}
-22
View File
@@ -1,22 +0,0 @@
package presenter
import (
"io"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/imgbom/imgbom/presenter/json"
"github.com/anchore/stereoscope/pkg/image"
)
type Presenter interface {
Present(io.Writer, *image.Image, pkg.Catalog) error
}
func GetPresenter(option Option) Presenter {
switch option {
case JSONPresenter:
return json.NewPresenter()
default:
return nil
}
}
-46
View File
@@ -1,46 +0,0 @@
package scope
import (
"fmt"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/tree"
)
type Scope struct {
Option Option
Trees []*tree.FileTree
Image *image.Image
}
func NewScope(img *image.Image, option Option) (Scope, error) {
var trees = make([]*tree.FileTree, 0)
if img == nil {
return Scope{}, fmt.Errorf("no image given")
}
switch option {
case SquashedScope:
if img.SquashedTree == nil {
return Scope{}, fmt.Errorf("the image does not have have a squashed tree")
}
trees = append(trees, img.SquashedTree)
case AllLayersScope:
if len(img.Layers) == 0 {
return Scope{}, fmt.Errorf("the image does not contain any layers")
}
for _, layer := range img.Layers {
trees = append(trees, layer.Tree)
}
default:
return Scope{}, fmt.Errorf("bad option provided: %+v", option)
}
return Scope{
Option: option,
Trees: trees,
Image: img,
}, nil
}
-91
View File
@@ -1,91 +0,0 @@
package scope
import (
"testing"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/tree"
)
func testScopeImage(t *testing.T) *image.Image {
t.Helper()
one := image.NewLayer(nil)
one.Tree = tree.NewFileTree()
one.Tree.AddPath("/tree/first/path.txt")
two := image.NewLayer(nil)
two.Tree = tree.NewFileTree()
two.Tree.AddPath("/tree/second/path.txt")
i := image.NewImage(nil)
i.Layers = []image.Layer{one, two}
err := i.Squash()
if err != nil {
t.Fatal("could not squash test image trees")
}
return i
}
func TestScope(t *testing.T) {
refImg := testScopeImage(t)
cases := []struct {
name string
img *image.Image
option Option
expectedTrees []*tree.FileTree
err bool
}{
{
name: "AllLayersGoCase",
option: AllLayersScope,
img: testScopeImage(t),
expectedTrees: []*tree.FileTree{refImg.Layers[0].Tree, refImg.Layers[1].Tree},
},
{
name: "SquashedGoCase",
option: SquashedScope,
img: testScopeImage(t),
expectedTrees: []*tree.FileTree{refImg.SquashedTree},
},
{
name: "MissingImage",
option: SquashedScope,
err: true,
},
{
name: "MissingSquashedTree",
option: SquashedScope,
img: image.NewImage(nil),
err: true,
},
{
name: "NoLayers",
option: AllLayersScope,
img: image.NewImage(nil),
err: true,
},
}
for _, c := range cases {
actual, err := NewScope(c.img, c.option)
if err == nil && c.err {
t.Fatal("expected an error but did not find one")
} else if err != nil && !c.err {
t.Fatal("expected no error but found one:", err)
}
if len(actual.Trees) != len(c.expectedTrees) {
t.Fatalf("mismatched tree lengths: %d!=%d", len(actual.Trees), len(c.expectedTrees))
}
for idx, at := range actual.Trees {
if !at.Equal(c.expectedTrees[idx]) {
t.Error("mismatched tree @ idx", idx)
}
}
}
}
Executable
+375
View File
@@ -0,0 +1,375 @@
#!/bin/sh
set -e
# Code generated by godownloader on 2020-08-10T20:55:46Z. DO NOT EDIT.
#
usage() {
this=$1
cat <<EOF
$this: download go binaries for anchore/syft
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/anchore/syft/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/amd64) BINARIES="syft" ;;
linux/amd64) BINARIES="syft" ;;
windows/amd64) BINARIES="syft" ;;
*)
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
true
}
adjust_arch() {
# adjust archive name based on ARCH
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="syft"
OWNER=anchore
REPO="syft"
BINARY=syft
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}_${VERSION}_${OS}_${ARCH}
TARBALL=${NAME}.${FORMAT}
TARBALL_URL=${GITHUB_DOWNLOAD}/${TAG}/${TARBALL}
CHECKSUM=${PROJECT_NAME}_${VERSION}_checksums.txt
CHECKSUM_URL=${GITHUB_DOWNLOAD}/${TAG}/${CHECKSUM}
execute
+35
View File
@@ -0,0 +1,35 @@
/*
Package bus provides access to a singleton instance of an event bus (provided by the calling application). The event bus
is intended to allow for the syft library to publish events which library consumers can subscribe to. These events
can provide static information, but also have an object as a payload for which the consumer can poll for updates.
This is akin to a logger, except instead of only allowing strings to be logged, rich objects that can be interacted with.
Note that the singleton instance is only allowed to publish events and not subscribe to them --this is intentional.
Internal library interactions should continue to use traditional in-execution-path approaches for data sharing
(e.g. function returns and channels) and not depend on bus subscriptions for critical interactions (e.g. one part of the
lib publishes an event and another part of the lib subscribes and reacts to that event). The bus is provided only as a
means for consumers to observe events emitted from the library (such as to provide a rich UI) and not to allow
consumers to augment or otherwise change execution.
*/
package bus
import "github.com/wagoodman/go-partybus"
var publisher partybus.Publisher
var active bool
// SetPublisher sets the singleton event bus publisher. This is optional; if no bus is provided, the library will
// behave no differently than if a bus had been provided.
func SetPublisher(p partybus.Publisher) {
publisher = p
if p != nil {
active = true
}
}
// Publish an event onto the bus. If there is no bus set by the calling application, this does nothing.
func Publish(event partybus.Event) {
if active {
publisher.Publish(event)
}
}
+31 -22
View File
@@ -6,12 +6,12 @@ import (
"strings"
"github.com/adrg/xdg"
"github.com/anchore/imgbom/imgbom/presenter"
"github.com/anchore/imgbom/imgbom/scope"
"github.com/anchore/imgbom/internal"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/scope"
"github.com/mitchellh/go-homedir"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"go.uber.org/zap/zapcore"
)
type CliOnlyOptions struct {
@@ -20,19 +20,26 @@ type CliOnlyOptions struct {
}
type Application struct {
ConfigPath string
PresenterOpt presenter.Option
Output string `mapstructure:"output"`
ScopeOpt scope.Option
Scope string `mapstructure:"scope"`
Quiet bool `mapstructure:"quiet"`
Log Logging `mapstructure:"log"`
CliOptions CliOnlyOptions
ConfigPath string
PresenterOpt presenter.Option
Output string `mapstructure:"output"`
ScopeOpt scope.Option
Scope string `mapstructure:"scope"`
Quiet bool `mapstructure:"quiet"`
Log Logging `mapstructure:"log"`
CliOptions CliOnlyOptions
CheckForAppUpdate bool `mapstructure:"check-for-app-update"`
Plugins Plugins `mapstructure:"plugins"`
}
type Plugins struct {
Enabled bool `mapstructure:"enabled"`
Directory string `mapstructure:"dir"`
}
type Logging struct {
Structured bool `mapstructure:"structured"`
LevelOpt zapcore.Level
LevelOpt logrus.Level
Level string `mapstructure:"level"`
FileLocation string `mapstructure:"file"`
}
@@ -41,6 +48,7 @@ func setNonCliDefaultValues(v *viper.Viper) {
v.SetDefault("log.level", "")
v.SetDefault("log.file", "")
v.SetDefault("log.structured", false)
v.SetDefault("check-for-app-update", true)
}
func LoadConfigFromFile(v *viper.Viper, cliOpts *CliOnlyOptions) (*Application, error) {
@@ -86,29 +94,30 @@ func (cfg *Application) Build() error {
if cfg.Quiet {
// TODO: this is bad: quiet option trumps all other logging options
// we should be able to quiet the consol logging and leave file logging alone...
// we should be able to quiet the console logging and leave file logging alone...
// ... this will be an enhancement for later
cfg.Log.LevelOpt = zapcore.PanicLevel
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")
}
// set the log level explicitly
err := cfg.Log.LevelOpt.Set(cfg.Log.Level)
lvl, err := logrus.ParseLevel(strings.ToLower(cfg.Log.Level))
if err != nil {
return fmt.Errorf("bad log level value '%s': %+v", cfg.Log.Level, err)
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 = zapcore.InfoLevel
cfg.Log.LevelOpt = logrus.InfoLevel
case v >= 2:
cfg.Log.LevelOpt = zapcore.DebugLevel
cfg.Log.LevelOpt = logrus.DebugLevel
default:
cfg.Log.LevelOpt = zapcore.ErrorLevel
cfg.Log.LevelOpt = logrus.ErrorLevel
}
}
}
@@ -121,7 +130,7 @@ func readConfig(v *viper.Viper, configPath string) error {
v.SetEnvPrefix(internal.ApplicationName)
// allow for nested options to be specified via environment variables
// e.g. pod.context = APPNAME_POD_CONTEXT
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
// use explicitly the given user config
if configPath != "" {
+2 -2
View File
@@ -1,4 +1,4 @@
package internal
// note: must be a single word, all lowercase
const ApplicationName = "imgbom"
// note: do not change this
const ApplicationName = "syft"
+45
View File
@@ -0,0 +1,45 @@
package file
// Source: https://research.swtch.com/glob.go
func GlobMatch(pattern, name string) bool {
px := 0
nx := 0
nextPx := 0
nextNx := 0
for px < len(pattern) || nx < len(name) {
if px < len(pattern) {
c := pattern[px]
switch c {
default: // ordinary character
if nx < len(name) && name[nx] == c {
px++
nx++
continue
}
case '?': // single-character wildcard
if nx < len(name) {
px++
nx++
continue
}
case '*': // zero-or-more-character wildcard
// Try to match at nx.
// If that doesn't work out,
// restart at nx+1 next.
nextPx = px
nextNx = nx + 1
px++
continue
}
}
// Mismatch. Maybe restart.
if 0 < nextNx && nextNx <= len(name) {
px = nextPx
nx = nextNx
continue
}
return false
}
// Matched all of pattern to all of name. Success.
return true
}
+39
View File
@@ -0,0 +1,39 @@
package file
import (
"strings"
"testing"
)
func TestGlobMatch(t *testing.T) {
var tests = []struct {
pattern string
data string
ok bool
}{
{"", "", true},
{"x", "", false},
{"", "x", false},
{"abc", "abc", true},
{"*", "abc", true},
{"*c", "abc", true},
{"*b", "abc", false},
{"a*", "abc", true},
{"b*", "abc", false},
{"a*", "a", true},
{"*a", "a", true},
{"a*b*c*d*e*", "axbxcxdxe", true},
{"a*b*c*d*e*", "axbxcxdxexxx", true},
{"a*b?c*x", "abxbbxdbxebxczzx", true},
{"a*b?c*x", "abxbbxdbxebxczzy", false},
{"a*a*a*a*b", strings.Repeat("a", 100), false},
{"*x", "xxx", true},
{"/home/place/**", "/home/place/a/thing", true},
}
for _, test := range tests {
if GlobMatch(test.pattern, test.data) != test.ok {
t.Errorf("failed glob='%s' data='%s'", test.pattern, test.data)
}
}
}
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -eux
zip -r "$1" zip-source
@@ -0,0 +1 @@
B file...
@@ -0,0 +1 @@
A file! nice!
+58
View File
@@ -0,0 +1,58 @@
package file
import (
"archive/zip"
"fmt"
"os"
"sort"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
)
type ZipFileManifest map[string]os.FileInfo
func newZipManifest() ZipFileManifest {
return make(ZipFileManifest)
}
func (z ZipFileManifest) Add(entry string, info os.FileInfo) {
z[entry] = info
}
func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
uniqueMatches := internal.NewStringSet()
for _, pattern := range patterns {
for entry := range z {
if GlobMatch(pattern, entry) {
uniqueMatches.Add(entry)
}
}
}
results := uniqueMatches.ToSlice()
sort.Strings(results)
return results
}
func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
zipReader, err := zip.OpenReader(archivePath)
manifest := newZipManifest()
if err != nil {
return manifest, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
}
defer func() {
err = zipReader.Close()
if err != nil {
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
}
}()
for _, file := range zipReader.Reader.File {
manifest.Add(file.Name, file.FileInfo())
}
return manifest, nil
}
+222
View File
@@ -0,0 +1,222 @@
package file
import (
"archive/zip"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/anchore/syft/internal/log"
)
const (
_ = iota
KB = 1 << (10 * iota)
MB
GB
)
const perFileReadLimit = 2 * GB
type zipTraversalRequest map[string]struct{}
func newZipTraverseRequest(paths ...string) zipTraversalRequest {
results := make(zipTraversalRequest)
for _, p := range paths {
results[p] = struct{}{}
}
return results
}
func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths ...string) error {
request := newZipTraverseRequest(paths...)
zipReader, err := zip.OpenReader(archivePath)
if err != nil {
return fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
}
defer func() {
err = zipReader.Close()
if err != nil {
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
}
}()
for _, file := range zipReader.Reader.File {
// if no paths are given then assume that all files should be traversed
if len(paths) > 0 {
if _, ok := request[file.Name]; !ok {
// this file path is not of interest
continue
}
}
if err = visitor(file); err != nil {
return err
}
}
return nil
}
func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (map[string]io.Reader, error) {
results := make(map[string]io.Reader)
// don't allow for full traversal, only select traversal from given paths
if len(paths) == 0 {
return results, nil
}
visitor := func(file *zip.File) error {
tempfilePrefix := filepath.Base(filepath.Clean(file.Name)) + "-"
tempFile, err := ioutil.TempFile(dir, tempfilePrefix)
if err != nil {
return fmt.Errorf("unable to create temp file: %w", err)
}
zippedFile, err := file.Open()
if err != nil {
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
}
if file.FileInfo().IsDir() {
return fmt.Errorf("unable to extract directories, only files: %s", file.Name)
}
// limit the zip reader on each file read to prevent decompression bomb attacks
numBytes, err := io.Copy(tempFile, io.LimitReader(zippedFile, perFileReadLimit))
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
if err != nil {
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
}
// the file pointer is at the end due to the copy operation, reset back to the beginning
_, err = tempFile.Seek(0, io.SeekStart)
if err != nil {
return fmt.Errorf("unable to reset file pointer (%s): %w", tempFile.Name(), err)
}
results[file.Name] = tempFile
err = zippedFile.Close()
if err != nil {
return fmt.Errorf("unable to close source file=%q from zip=%q: %w", file.Name, archivePath, err)
}
return nil
}
return results, TraverseFilesInZip(archivePath, visitor, paths...)
}
func ContentsFromZip(archivePath string, paths ...string) (map[string]string, error) {
results := make(map[string]string)
// don't allow for full traversal, only select traversal from given paths
if len(paths) == 0 {
return results, nil
}
visitor := func(file *zip.File) error {
zippedFile, err := file.Open()
if err != nil {
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
}
if file.FileInfo().IsDir() {
return fmt.Errorf("unable to extract directories, only files: %s", file.Name)
}
var buffer bytes.Buffer
// limit the zip reader on each file read to prevent decompression bomb attacks
numBytes, err := io.Copy(&buffer, io.LimitReader(zippedFile, perFileReadLimit))
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
if err != nil {
return fmt.Errorf("unable to copy source=%q for zip=%q: %w", file.Name, archivePath, err)
}
results[file.Name] = buffer.String()
err = zippedFile.Close()
if err != nil {
return fmt.Errorf("unable to close source file=%q from zip=%q: %w", file.Name, archivePath, err)
}
return nil
}
return results, TraverseFilesInZip(archivePath, visitor, paths...)
}
func UnzipToDir(archivePath, targetDir string) error {
visitor := func(file *zip.File) error {
// the zip-slip attack protection is still being erroneously detected
// nolint:gosec
expandedFilePath := filepath.Clean(filepath.Join(targetDir, file.Name))
// protect against zip slip attacks (traversing unintended parent paths from maliciously crafted relative-path entries)
if !strings.HasPrefix(expandedFilePath, filepath.Clean(targetDir)+string(os.PathSeparator)) {
return fmt.Errorf("potential zip slip attack: %q", expandedFilePath)
}
err := extractSingleFile(file, expandedFilePath, archivePath)
if err != nil {
return err
}
return nil
}
return TraverseFilesInZip(archivePath, visitor)
}
func extractSingleFile(file *zip.File, expandedFilePath, archivePath string) error {
zippedFile, err := file.Open()
if err != nil {
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
}
if file.FileInfo().IsDir() {
err = os.MkdirAll(expandedFilePath, file.Mode())
if err != nil {
return fmt.Errorf("unable to create dir=%q from zip=%q: %w", expandedFilePath, archivePath, err)
}
} else {
// Open an output file for writing
outputFile, err := os.OpenFile(
expandedFilePath,
os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
file.Mode(),
)
if err != nil {
return fmt.Errorf("unable to create dest file=%q from zip=%q: %w", expandedFilePath, archivePath, err)
}
// limit the zip reader on each file read to prevent decompression bomb attacks
numBytes, err := io.Copy(outputFile, io.LimitReader(zippedFile, perFileReadLimit))
if numBytes >= perFileReadLimit || errors.Is(err, io.EOF) {
return fmt.Errorf("zip read limit hit (potential decompression bomb attack)")
}
if err != nil {
return fmt.Errorf("unable to copy source=%q to dest=%q for zip=%q: %w", file.Name, outputFile.Name(), archivePath, err)
}
err = outputFile.Close()
if err != nil {
return fmt.Errorf("unable to close dest file=%q from zip=%q: %w", outputFile.Name(), archivePath, err)
}
}
err = zippedFile.Close()
if err != nil {
return fmt.Errorf("unable to close source file=%q from zip=%q: %w", file.Name, archivePath, err)
}
return nil
}
+266
View File
@@ -0,0 +1,266 @@
package file
import (
"crypto/sha256"
"encoding/json"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"testing"
"github.com/go-test/deep"
)
func generateFixture(t *testing.T, archivePath string) {
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
cmd := exec.Command("./generate-zip-fixture.sh", archivePath)
cmd.Dir = filepath.Join(cwd, "test-fixtures")
if err := cmd.Start(); err != nil {
t.Fatalf("unable to start generate zip fixture script: %+v", err)
}
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
// This works on both Unix and Windows. Although package
// syscall is generally platform dependent, WaitStatus is
// defined for both Unix and Windows and in both cases has
// an ExitStatus() method with the same signature.
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
if status.ExitStatus() != 0 {
t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
}
}
} else {
t.Fatalf("unable to get generate fixture script result: %+v", err)
}
}
}
func equal(r1, r2 io.Reader) (bool, error) {
w1 := sha256.New()
w2 := sha256.New()
n1, err1 := io.Copy(w1, r1)
if err1 != nil {
return false, err1
}
n2, err2 := io.Copy(w2, r2)
if err2 != nil {
return false, err2
}
var b1, b2 [sha256.Size]byte
copy(b1[:], w1.Sum(nil))
copy(b2[:], w2.Sum(nil))
return n1 != n2 || b1 == b2, nil
}
func TestUnzipToDir(t *testing.T) {
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
if err != nil {
t.Fatalf("unable to create tempfile: %+v", err)
}
defer os.Remove(archivePrefix.Name())
// the zip utility will add ".zip" to the end of the given name
archivePath := archivePrefix.Name() + ".zip"
defer os.Remove(archivePath)
t.Logf("archive path: %s", archivePath)
generateFixture(t, archivePrefix.Name())
contentsDir, err := ioutil.TempDir("", "syft-ziputil-contents-TEST-")
if err != nil {
t.Fatalf("unable to create tempdir: %+v", err)
}
defer os.RemoveAll(contentsDir)
t.Logf("content path: %s", contentsDir)
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
t.Logf("running from: %s", cwd)
// note: zip utility already includes "zip-source" as a parent dir for all contained files
goldenRootDir := filepath.Join(cwd, "test-fixtures")
expectedPaths := 4
observedPaths := 0
err = UnzipToDir(archivePath, contentsDir)
if err != nil {
t.Fatalf("unable to unzip archive: %+v", err)
}
// compare the source dir tree and the unzipped tree
err = filepath.Walk(filepath.Join(contentsDir, "zip-source"),
func(path string, info os.FileInfo, err error) error {
t.Logf("unzipped path: %s", path)
observedPaths++
if err != nil {
t.Fatalf("this should not happen")
return err
}
goldenPath := filepath.Join(goldenRootDir, strings.TrimPrefix(path, contentsDir))
if info.IsDir() {
i, err := os.Stat(goldenPath)
if err != nil {
t.Fatalf("unable to stat golden path: %+v", err)
}
if !i.IsDir() {
t.Fatalf("mismatched file types: %s", goldenPath)
}
return nil
}
// this is a file, not a dir...
testFile, err := os.Open(path)
if err != nil {
t.Fatalf("unable to open test file=%s :%+v", path, err)
}
goldenFile, err := os.Open(goldenPath)
if err != nil {
t.Fatalf("unable to open golden file=%s :%+v", goldenPath, err)
}
same, err := equal(testFile, goldenFile)
if err != nil {
t.Fatalf("could not compare files (%s, %s): %+v", goldenPath, path, err)
}
if !same {
t.Errorf("paths are not the same (%s, %s)", goldenPath, path)
}
return nil
})
if err != nil {
t.Errorf("failed to walk dir: %+v", err)
}
if observedPaths != expectedPaths {
t.Errorf("missed test paths: %d!=%d", observedPaths, expectedPaths)
}
}
func TestExtractFilesFromZipFile(t *testing.T) {
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
if err != nil {
t.Fatalf("unable to create tempfile: %+v", err)
}
defer os.Remove(archivePrefix.Name())
// the zip utility will add ".zip" to the end of the given name
archivePath := archivePrefix.Name() + ".zip"
defer os.Remove(archivePath)
t.Logf("archive path: %s", archivePath)
generateFixture(t, archivePrefix.Name())
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
t.Logf("running from: %s", cwd)
aFilePath := filepath.Join("zip-source", "some-dir", "a-file.txt")
bFilePath := filepath.Join("zip-source", "b-file.txt")
expected := map[string]string{
aFilePath: "A file! nice!",
bFilePath: "B file...",
}
actual, err := ContentsFromZip(archivePath, aFilePath, bFilePath)
if err != nil {
t.Fatalf("unable to extract from unzip archive: %+v", err)
}
diffs := deep.Equal(actual, expected)
if len(diffs) > 0 {
for _, d := range diffs {
t.Errorf("diff: %+v", d)
}
b, err := json.MarshalIndent(actual, "", " ")
if err != nil {
t.Fatalf("can't show results: %+v", err)
}
t.Errorf("full result: %s", string(b))
}
}
func TestZipFileManifest(t *testing.T) {
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
if err != nil {
t.Fatalf("unable to create tempfile: %+v", err)
}
defer os.Remove(archivePrefix.Name())
// the zip utility will add ".zip" to the end of the given name
archivePath := archivePrefix.Name() + ".zip"
defer os.Remove(archivePath)
t.Logf("archive path: %s", archivePath)
generateFixture(t, archivePrefix.Name())
cwd, err := os.Getwd()
if err != nil {
t.Errorf("unable to get cwd: %+v", err)
}
t.Logf("running from: %s", cwd)
expected := []string{
filepath.Join("zip-source") + string(os.PathSeparator),
filepath.Join("zip-source", "some-dir") + string(os.PathSeparator),
filepath.Join("zip-source", "some-dir", "a-file.txt"),
filepath.Join("zip-source", "b-file.txt"),
}
actual, err := NewZipFileManifest(archivePath)
if err != nil {
t.Fatalf("unable to extract from unzip archive: %+v", err)
}
if len(expected) != len(actual) {
t.Fatalf("mismatched manifest: %d != %d", len(actual), len(expected))
}
for _, e := range expected {
_, ok := actual[e]
if !ok {
t.Errorf("missing path: %s", e)
}
}
if t.Failed() {
b, err := json.MarshalIndent(actual, "", " ")
if err != nil {
t.Fatalf("can't show results: %+v", err)
}
t.Errorf("full result: %s", string(b))
}
}
+7 -1
View File
@@ -1,6 +1,8 @@
package log
import "github.com/anchore/imgbom/imgbom/logger"
import (
"github.com/anchore/syft/syft/logger"
)
var Log logger.Logger = &nopLogger{}
@@ -8,6 +10,10 @@ func Errorf(format string, args ...interface{}) {
Log.Errorf(format, args...)
}
func Error(args ...interface{}) {
Log.Error(args...)
}
func Infof(format string, args ...interface{}) {
Log.Infof(format, args...)
}
+1
View File
@@ -3,6 +3,7 @@ package log
type nopLogger struct{}
func (l *nopLogger) Errorf(format string, args ...interface{}) {}
func (l *nopLogger) Error(args ...interface{}) {}
func (l *nopLogger) Infof(format string, args ...interface{}) {}
func (l *nopLogger) Info(args ...interface{}) {}
func (l *nopLogger) Debugf(format string, args ...interface{}) {}
+78
View File
@@ -0,0 +1,78 @@
package logger
import (
"bytes"
"strings"
"github.com/hashicorp/go-hclog"
)
// Source: https://github.com/hashicorp/go-hclog/blob/master/stdlog.go (MIT License)
// Provides a io.Writer to shim the data out of *log.Logger
// and back into our Logger. This is basically the only way to
// build upon *log.Logger.
type HCLogStdlogAdapter struct {
log hclog.Logger
inferLevels bool
forceLevel hclog.Level
}
// Take the data, infer the levels if configured, and send it through
// a regular Logger.
func (s *HCLogStdlogAdapter) Write(data []byte) (int, error) {
str := string(bytes.TrimRight(data, " \t\n"))
if s.forceLevel != hclog.NoLevel {
// Use pickLevel to strip log levels included in the line since we are
// forcing the level
_, str := s.pickLevel(str)
// Log at the forced level
s.dispatch(str, s.forceLevel)
} else if s.inferLevels {
level, str := s.pickLevel(str)
s.dispatch(str, level)
} else {
s.log.Info(str)
}
return len(data), nil
}
func (s *HCLogStdlogAdapter) dispatch(str string, level hclog.Level) {
switch level {
case hclog.Trace:
s.log.Trace(str)
case hclog.Debug:
s.log.Debug(str)
case hclog.Info:
s.log.Info(str)
case hclog.Warn:
s.log.Warn(str)
case hclog.Error:
s.log.Error(str)
default:
s.log.Info(str)
}
}
// Detect, based on conventions, what log level this is.
func (s *HCLogStdlogAdapter) pickLevel(str string) (hclog.Level, string) {
switch {
case strings.HasPrefix(str, "[DEBUG]"):
return hclog.Debug, strings.TrimSpace(str[7:])
case strings.HasPrefix(str, "[TRACE]"):
return hclog.Trace, strings.TrimSpace(str[7:])
case strings.HasPrefix(str, "[INFO]"):
return hclog.Info, strings.TrimSpace(str[6:])
case strings.HasPrefix(str, "[WARN]"):
return hclog.Warn, strings.TrimSpace(str[7:])
case strings.HasPrefix(str, "[ERROR]"):
return hclog.Error, strings.TrimSpace(str[7:])
case strings.HasPrefix(str, "[ERR]"):
return hclog.Error, strings.TrimSpace(str[5:])
default:
return hclog.Info, str
}
}
+148
View File
@@ -0,0 +1,148 @@
package logger
import (
"io"
"log"
"github.com/anchore/syft/syft/logger"
"github.com/hashicorp/go-hclog"
)
// integrity check
var _ hclog.Logger = &LoggerHCLogAdapter{}
// LoggerHCLogAdapter is used to (partially) adapt a syft logger.Logger object to the hclog.Logger interface for syft plugins.
// Note: this does not implement all functionality required by the hclog.Logger.
type LoggerHCLogAdapter struct {
Logger logger.Logger
}
func NewLoggerHCLogAdapter(logger logger.Logger) *LoggerHCLogAdapter {
return &LoggerHCLogAdapter{
Logger: logger,
}
}
// Args are alternating key, val pairs
// keys must be strings
// vals can be any type, but display is implementation specific
// Emit a message and key/value pairs at a provided log level
func (l *LoggerHCLogAdapter) Log(level hclog.Level, msg string, args ...interface{}) {
l.Logger.Info(append([]interface{}{msg}, args...)...)
}
// Emit a message and key/value pairs at the TRACE level
func (l *LoggerHCLogAdapter) Trace(msg string, args ...interface{}) {
l.Logger.Debug(append([]interface{}{msg}, args...)...)
}
// Emit a message and key/value pairs at the DEBUG level
func (l *LoggerHCLogAdapter) Debug(msg string, args ...interface{}) {
l.Logger.Debug(append([]interface{}{msg}, args...)...)
}
// Emit a message and key/value pairs at the INFO level
func (l *LoggerHCLogAdapter) Info(msg string, args ...interface{}) {
l.Logger.Info(append([]interface{}{msg}, args...)...)
}
// Emit a message and key/value pairs at the WARN level
func (l *LoggerHCLogAdapter) Warn(msg string, args ...interface{}) {
l.Logger.Error(append([]interface{}{msg}, args...)...)
}
// Emit a message and key/value pairs at the ERROR level
func (l *LoggerHCLogAdapter) Error(msg string, args ...interface{}) {
l.Logger.Error(append([]interface{}{msg}, args...)...)
}
// Indicate if TRACE logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LoggerHCLogAdapter) IsTrace() bool {
// TODO: not implemented
return true
}
// Indicate if DEBUG logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LoggerHCLogAdapter) IsDebug() bool {
// TODO: not implemented
return true
}
// Indicate if INFO logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LoggerHCLogAdapter) IsInfo() bool {
// TODO: not implemented
return true
}
// Indicate if WARN logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LoggerHCLogAdapter) IsWarn() bool {
// TODO: not implemented
return true
}
// Indicate if ERROR logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LoggerHCLogAdapter) IsError() bool {
// TODO: not implemented
return true
}
// ImpliedArgs returns With key/value pairs
func (l *LoggerHCLogAdapter) ImpliedArgs() []interface{} {
// TODO: not implemented
return nil
}
// Creates a sublogger that will always have the given key/value pairs
func (l *LoggerHCLogAdapter) With(args ...interface{}) hclog.Logger {
// TODO: not implemented
return l
}
// Returns the Name of the logger
func (l *LoggerHCLogAdapter) Name() string {
// TODO: not implemented
return ""
}
// Create a logger that will prepend the name string on the front of all messages.
// If the logger already has a name, the new value will be appended to the current
// name. That way, a major subsystem can use this to decorate all it's own logs
// without losing context.
func (l *LoggerHCLogAdapter) Named(name string) hclog.Logger {
// TODO: not implemented
return l
}
// Create a logger that will prepend the name string on the front of all messages.
// This sets the name of the logger to the value directly, unlike Named which honor
// the current name as well.
func (l *LoggerHCLogAdapter) ResetNamed(name string) hclog.Logger {
// TODO: not implemented
return l
}
// Updates the level. This should affect all sub-loggers as well. If an
// implementation cannot update the level on the fly, it should no-op.
func (l *LoggerHCLogAdapter) SetLevel(level hclog.Level) {
// TODO: not implemented
}
// Return a value that conforms to the stdlib log.Logger interface
func (l *LoggerHCLogAdapter) StandardLogger(opts *hclog.StandardLoggerOptions) *log.Logger {
return log.New(l.StandardWriter(opts), "", log.LstdFlags)
}
// Return a value that conforms to io.Writer, which can be passed into log.SetOutput()
func (l *LoggerHCLogAdapter) StandardWriter(opts *hclog.StandardLoggerOptions) io.Writer {
return &HCLogStdlogAdapter{
log: l,
inferLevels: opts.InferLevels,
forceLevel: opts.ForceLevel,
}
}
+130
View File
@@ -0,0 +1,130 @@
package logger
import (
"fmt"
"io"
"io/ioutil"
"os"
"github.com/anchore/syft/syft/logger"
"github.com/sirupsen/logrus"
prefixed "github.com/x-cray/logrus-prefixed-formatter"
)
// integrity check
var _ logger.Logger = &LogrusLogger{}
var _ logger.Logger = &LogrusNestedLogger{}
type LogrusConfig struct {
EnableConsole bool
EnableFile bool
Structured bool
Level logrus.Level
FileLocation string
}
type LogrusLogger struct {
Config LogrusConfig
Logger *logrus.Logger
Output io.Writer
}
type LogrusNestedLogger struct {
Logger *logrus.Entry
}
func NewLogrusLogger(cfg LogrusConfig) *LogrusLogger {
appLogger := logrus.New()
var output io.Writer
switch {
case cfg.EnableConsole && cfg.EnableFile:
logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, 0755)
if err != nil {
panic(fmt.Errorf("unable to setup log file: %w", err))
}
output = io.MultiWriter(os.Stderr, logFile)
case cfg.EnableConsole:
output = os.Stderr
case cfg.EnableFile:
logFile, err := os.OpenFile(cfg.FileLocation, os.O_WRONLY|os.O_CREATE, 0755)
if err != nil {
panic(fmt.Errorf("unable to setup log file: %w", err))
}
output = logFile
default:
output = ioutil.Discard
}
appLogger.SetOutput(output)
appLogger.SetLevel(cfg.Level)
if cfg.Structured {
appLogger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
DisableTimestamp: false,
DisableHTMLEscape: false,
PrettyPrint: false,
})
} else {
appLogger.SetFormatter(&prefixed.TextFormatter{
TimestampFormat: "2006-01-02 15:04:05",
ForceColors: true,
ForceFormatting: true,
})
}
return &LogrusLogger{
Config: cfg,
Logger: appLogger,
Output: output,
}
}
func (l *LogrusLogger) Errorf(format string, args ...interface{}) {
l.Logger.Errorf(format, args...)
}
func (l *LogrusLogger) Infof(format string, args ...interface{}) {
l.Logger.Infof(format, args...)
}
func (l *LogrusLogger) Debugf(format string, args ...interface{}) {
l.Logger.Debugf(format, args...)
}
func (l *LogrusLogger) Error(args ...interface{}) {
l.Logger.Error(args...)
}
func (l *LogrusLogger) Info(args ...interface{}) {
l.Logger.Info(args...)
}
func (l *LogrusLogger) Debug(args ...interface{}) {
l.Logger.Debug(args...)
}
func (l *LogrusNestedLogger) Errorf(format string, args ...interface{}) {
l.Logger.Errorf(format, args...)
}
func (l *LogrusNestedLogger) Infof(format string, args ...interface{}) {
l.Logger.Infof(format, args...)
}
func (l *LogrusNestedLogger) Debugf(format string, args ...interface{}) {
l.Logger.Debugf(format, args...)
}
func (l *LogrusNestedLogger) Error(args ...interface{}) {
l.Logger.Error(args...)
}
func (l *LogrusNestedLogger) Info(args ...interface{}) {
l.Logger.Info(args...)
}
func (l *LogrusNestedLogger) Debug(args ...interface{}) {
l.Logger.Debug(args...)
}
+181
View File
@@ -0,0 +1,181 @@
package logger
import (
"fmt"
"io"
"log"
"github.com/hashicorp/go-hclog"
"github.com/sirupsen/logrus"
)
// integrity check
var _ hclog.Logger = &LogrusHCLogAdapter{}
var levels = map[hclog.Level]logrus.Level{
hclog.NoLevel: logrus.PanicLevel,
hclog.Error: logrus.ErrorLevel,
hclog.Warn: logrus.WarnLevel,
hclog.Info: logrus.InfoLevel,
hclog.Debug: logrus.DebugLevel,
hclog.Trace: logrus.TraceLevel,
}
// LogrusHCLogAdapter is used to adapt a logrus logger object to the hclog.Logger interface for syft plugins.
type LogrusHCLogAdapter struct {
logger *logrus.Logger
entry *logrus.Entry
}
func NewLogrusHCLogAdapter(logger *logrus.Logger, fields map[string]interface{}) *LogrusHCLogAdapter {
if fields != nil {
return &LogrusHCLogAdapter{
logger: logger,
entry: logger.WithFields(fields),
}
}
return &LogrusHCLogAdapter{
logger: logger,
}
}
func (l *LogrusHCLogAdapter) log(level hclog.Level, msg string, args ...interface{}) {
if l.entry != nil {
l.entry.WithFields(makeArgFields(args...)).Log(levels[level], msg)
return
}
l.logger.WithFields(makeArgFields(args...)).Log(levels[level], msg)
}
// Args are alternating key, val pairs
// keys must be strings
// vals can be any type, but display is implementation specific
// Emit a message and key/value pairs at a provided log level
func (l *LogrusHCLogAdapter) Log(level hclog.Level, msg string, args ...interface{}) {
l.log(level, msg, args...)
}
// Emit a message and key/value pairs at the TRACE level
func (l *LogrusHCLogAdapter) Trace(msg string, args ...interface{}) {
l.log(hclog.Trace, msg, args...)
}
// Emit a message and key/value pairs at the DEBUG level
func (l *LogrusHCLogAdapter) Debug(msg string, args ...interface{}) {
l.log(hclog.Debug, msg, args...)
}
// Emit a message and key/value pairs at the INFO level
func (l *LogrusHCLogAdapter) Info(msg string, args ...interface{}) {
l.log(hclog.Info, msg, args...)
}
// Emit a message and key/value pairs at the WARN level
func (l *LogrusHCLogAdapter) Warn(msg string, args ...interface{}) {
l.log(hclog.Warn, msg, args...)
}
// Emit a message and key/value pairs at the ERROR level
func (l *LogrusHCLogAdapter) Error(msg string, args ...interface{}) {
l.log(hclog.Error, msg, args...)
}
// Indicate if TRACE logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LogrusHCLogAdapter) IsTrace() bool {
return l.logger.Level <= logrus.TraceLevel
}
// Indicate if DEBUG logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LogrusHCLogAdapter) IsDebug() bool {
return l.logger.Level <= logrus.DebugLevel
}
// Indicate if INFO logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LogrusHCLogAdapter) IsInfo() bool {
return l.logger.Level <= logrus.InfoLevel
}
// Indicate if WARN logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LogrusHCLogAdapter) IsWarn() bool {
return l.logger.Level <= logrus.WarnLevel
}
// Indicate if ERROR logs would be emitted. This and the other Is* guards
// are used to elide expensive logging code based on the current level.
func (l *LogrusHCLogAdapter) IsError() bool {
return l.logger.Level <= logrus.ErrorLevel
}
// ImpliedArgs returns With key/value pairs
func (l *LogrusHCLogAdapter) ImpliedArgs() []interface{} {
// TODO: not implemented
return nil
}
// Creates a sublogger that will always have the given key/value pairs
func (l *LogrusHCLogAdapter) With(args ...interface{}) hclog.Logger {
l.entry = l.logger.WithFields(makeArgFields(args...))
return l
}
// Returns the Name of the logger
func (l *LogrusHCLogAdapter) Name() string {
// TODO: not implemented
return ""
}
// Create a logger that will prepend the name string on the front of all messages.
// If the logger already has a name, the new value will be appended to the current
// name. That way, a major subsystem can use this to decorate all it's own logs
// without losing context.
func (l *LogrusHCLogAdapter) Named(name string) hclog.Logger {
return NewLogrusHCLogAdapter(l.logger, logrus.Fields{"name": name})
}
// Create a logger that will prepend the name string on the front of all messages.
// This sets the name of the logger to the value directly, unlike Named which honor
// the current name as well.
func (l *LogrusHCLogAdapter) ResetNamed(name string) hclog.Logger {
return NewLogrusHCLogAdapter(l.logger, logrus.Fields{"name": name})
}
// Updates the level. This should affect all sub-loggers as well. If an
// implementation cannot update the level on the fly, it should no-op.
func (l *LogrusHCLogAdapter) SetLevel(level hclog.Level) {
l.logger.SetLevel(levels[level])
}
// Return a value that conforms to the stdlib log.Logger interface
func (l *LogrusHCLogAdapter) StandardLogger(opts *hclog.StandardLoggerOptions) *log.Logger {
return log.New(l.StandardWriter(opts), "", log.LstdFlags)
}
// Return a value that conforms to io.Writer, which can be passed into log.SetOutput()
func (l *LogrusHCLogAdapter) StandardWriter(opts *hclog.StandardLoggerOptions) io.Writer {
return &HCLogStdlogAdapter{
log: l,
inferLevels: opts.InferLevels,
forceLevel: opts.ForceLevel,
}
}
func makeArgFields(args ...interface{}) logrus.Fields {
if len(args)%2 != 0 {
panic(fmt.Errorf("odd number of logger key-value pairs (%d): %+v", len(args), args))
}
var key string
var fields = make(logrus.Fields)
for i, arg := range args {
if i%2 == 0 {
key = arg.(string)
} else {
fields[key] = arg
}
}
return fields
}
-141
View File
@@ -1,141 +0,0 @@
package logger
import (
"os"
"github.com/anchore/imgbom/internal/format"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
var levelToColor = map[zapcore.Level]format.Color{
zapcore.DebugLevel: format.Magenta,
zapcore.InfoLevel: format.Blue,
zapcore.WarnLevel: format.Yellow,
zapcore.ErrorLevel: format.Red,
zapcore.DPanicLevel: format.Red,
zapcore.PanicLevel: format.Red,
zapcore.FatalLevel: format.Red,
}
type LogConfig struct {
EnableConsole bool
EnableFile bool
Structured bool
Level zapcore.Level
FileLocation string
}
type ZapLogger struct {
Config LogConfig
Logger *zap.SugaredLogger
}
// TODO: Consider a human readable text encoder for better field handeling:
// - https://github.com/uber-go/zap/issues/570
// - https://github.com/uber-go/zap/pull/123
// - TextEncoder w/ old interface: https://github.com/uber-go/zap/blob/6c2107996402d47d559199b78e1c44747fe732f9/text_encoder.go
// - New interface example: https://github.com/uber-go/zap/blob/c2633d6de2d6e1170ad8f150660e3cf5310067c8/zapcore/json_encoder.go
// - Register the encoder: https://github.com/uber-go/zap/blob/v1.15.0/encoder.go
func NewZapLogger(config LogConfig) *ZapLogger {
appLogger := ZapLogger{
Config: config,
}
cores := []zapcore.Core{}
if config.EnableConsole {
// note: the report should go to stdout, all logs should go to stderr
writer := zapcore.Lock(os.Stderr)
core := zapcore.NewCore(appLogger.getConsoleEncoder(config), writer, config.Level)
cores = append(cores, core)
}
if config.EnableFile {
writer := zapcore.AddSync(appLogger.logFileWriter(config.FileLocation))
core := zapcore.NewCore(appLogger.fileEncoder(config), writer, config.Level)
cores = append(cores, core)
}
combinedCore := zapcore.NewTee(cores...)
// AddCallerSkip skips 2 number of callers, this is important else the file that gets
// logged will always be the wrapped file (In our case logger.go)
appLogger.Logger = zap.New(
combinedCore,
zap.AddCallerSkip(2),
zap.AddCaller(),
).Sugar()
return &appLogger
}
func (l *ZapLogger) GetNamedLogger(name string) *ZapLogger {
return &ZapLogger{
Logger: l.Logger.Named(name),
}
}
func (l *ZapLogger) getConsoleEncoder(config LogConfig) zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
if config.Structured {
encoderConfig.EncodeName = zapcore.FullNameEncoder
encoderConfig.EncodeCaller = zapcore.FullCallerEncoder
return zapcore.NewJSONEncoder(encoderConfig)
}
encoderConfig.EncodeTime = nil
encoderConfig.EncodeCaller = nil
encoderConfig.EncodeLevel = l.consoleLevelEncoder
encoderConfig.EncodeName = l.nameEncoder
return zapcore.NewConsoleEncoder(encoderConfig)
}
func (l *ZapLogger) nameEncoder(loggerName string, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString("[" + loggerName + "]")
}
func (l *ZapLogger) consoleLevelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
if level != zapcore.InfoLevel || l.Config.Level == zapcore.DebugLevel {
color, ok := levelToColor[level]
if !ok {
enc.AppendString("[" + level.CapitalString() + "]")
} else {
enc.AppendString("[" + color.Format(level.CapitalString()) + "]")
}
}
}
func (l *ZapLogger) fileEncoder(config LogConfig) zapcore.Encoder {
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeName = zapcore.FullNameEncoder
encoderConfig.EncodeCaller = zapcore.FullCallerEncoder
if config.Structured {
return zapcore.NewJSONEncoder(encoderConfig)
}
return zapcore.NewConsoleEncoder(encoderConfig)
}
func (l *ZapLogger) logFileWriter(location string) zapcore.WriteSyncer {
file, _ := os.Create(location)
return zapcore.AddSync(file)
}
func (l *ZapLogger) Debugf(format string, args ...interface{}) {
l.Logger.Debugf(format, args...)
}
func (l *ZapLogger) Infof(format string, args ...interface{}) {
l.Logger.Infof(format, args...)
}
func (l *ZapLogger) Debug(args ...interface{}) {
l.Logger.Debug(args...)
}
func (l *ZapLogger) Info(args ...interface{}) {
l.Logger.Info(args...)
}
func (l *ZapLogger) Errorf(format string, args ...interface{}) {
l.Logger.Errorf(format, args...)
}
+38
View File
@@ -0,0 +1,38 @@
package internal
type StringSet map[string]struct{}
func NewStringSet() StringSet {
return make(StringSet)
}
func NewStringSetFromSlice(start []string) StringSet {
ret := make(StringSet)
for _, s := range start {
ret.Add(s)
}
return ret
}
func (s StringSet) Add(i string) {
s[i] = struct{}{}
}
func (s StringSet) Remove(i string) {
delete(s, i)
}
func (s StringSet) Contains(i string) bool {
_, ok := s[i]
return ok
}
func (s StringSet) ToSlice() []string {
ret := make([]string, len(s))
idx := 0
for v := range s {
ret[idx] = v
idx++
}
return ret
}
+24
View File
@@ -0,0 +1,24 @@
package common
import (
"fmt"
"os"
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
"github.com/wagoodman/go-partybus"
)
// CatalogerFinishedHandler is a UI function for processing the CatalogerFinished bus event, displaying the catalog
// via the given presenter to stdout.
func CatalogerFinishedHandler(event partybus.Event) error {
// show the report to stdout
pres, err := syftEventParsers.ParseCatalogerFinished(event)
if err != nil {
return fmt.Errorf("bad CatalogerFinished event: %w", err)
}
if err := pres.Present(os.Stdout); err != nil {
return fmt.Errorf("unable to show package catalog report: %w", err)
}
return nil
}
+72
View File
@@ -0,0 +1,72 @@
package common
import (
"strings"
"sync"
)
// TODO: move me to a common module (used in multiple repos)
const (
SpinnerCircleOutlineSet = "◜◠◯◎◉●◉◎◯◡◞"
SpinnerCircleSet = "◌◯◎◉●◉◎◯"
SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
SpinnerHorizontalBarSet = "▉▊▋▌▍▎▏▎▍▌▋▊▉"
SpinnerVerticalBarSet = "▁▃▄▅▆▇█▇▆▅▄▃▁"
SpinnerDoubleBarSet = "▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▏▎▍▌▋▊▉█▇▆▅▄▃▂▁"
SpinnerArrowSet = "←↖↑↗→↘↓↙"
)
var SpinnerCircleDotSet = []string{
"⠈⠁",
"⠈⠑",
"⠈⠱",
"⠈⡱",
"⢀⡱",
"⢄⡱",
"⢄⡱",
"⢆⡱",
"⢎⡱",
"⢎⡰",
"⢎⡠",
"⢎⡀",
"⢎⠁",
"⠎⠁",
"⠊⠁",
}
type Spinner struct {
index int
charset []string
lock sync.Mutex
}
func NewSpinner(charset string) Spinner {
return Spinner{
charset: strings.Split(charset, ""),
}
}
func NewSpinnerFromSlice(charset []string) Spinner {
return Spinner{
charset: charset,
}
}
func (s *Spinner) Current() string {
s.lock.Lock()
defer s.lock.Unlock()
return s.charset[s.index]
}
func (s *Spinner) Next() string {
s.lock.Lock()
defer s.lock.Unlock()
c := s.charset[s.index]
s.index++
if s.index >= len(s.charset) {
s.index = 0
}
return c
}
+161
View File
@@ -0,0 +1,161 @@
/*
Package etui provides an "ephemeral" terminal user interface to display the application state dynamically.
The terminal is placed into raw mode and the cursor is manipulated to allow for a dynamic, multi-line
UI (provided by the jotframe lib), for this reason all other application mechanisms that write to the screen
must be suppressed before starting (such as logs); since bytes in the device and in application memory combine to make
a shared state, bytes coming from elsewhere to the screen will disrupt this state.
This UI is primarily driven off of events from the event bus, creating single-line terminal widgets to represent a
published element on the event bus, typically polling the element for the latest state. This allows for the UI to
control update frequency to the screen, provide "liveness" indications that are interpolated between bus events,
and overall loosely couple the bus events from screen interactions.
By convention, all elements published on the bus should be treated as read-only, and publishers on the bus should
attempt to enforce this when possible by wrapping complex objects with interfaces to prescribe interactions. Also by
convention, each new event that the UI should respond to should be added either in this package as a handler function,
or in the shared ui package as a function on the main handler object. All handler functions should be completed
processing an event before the ETUI exits (coordinated with a sync.WaitGroup)
*/
package etui
import (
"bytes"
"context"
"fmt"
"os"
"sync"
"github.com/anchore/syft/internal/logger"
"github.com/anchore/syft/internal/ui/common"
"github.com/anchore/syft/ui"
"github.com/anchore/syft/internal/log"
syftEvent "github.com/anchore/syft/syft/event"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/jotframe/pkg/frame"
)
// TODO: specify per-platform implementations with build tags (needed when windows is supported by syft)
// setupScreen creates a new jotframe object to manage specific screen lines dynamically, preparing the screen device
// as needed (i.e. setting the terminal to raw mode).
func setupScreen(output *os.File) *frame.Frame {
config := frame.Config{
PositionPolicy: frame.PolicyFloatForward,
// only report output to stderr, reserve report output for stdout
Output: output,
}
fr, err := frame.New(config)
if err != nil {
log.Errorf("failed to create screen object: %+v", err)
return nil
}
return fr
}
// nolint:funlen,gocognit
// OutputToEphemeralTUI is a UI function that provides a terminal UI experience without a separate, in-application
// screen buffer. All logging is suppressed, buffered to a string, and output after the ETUI has been torn down.
func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscription) error {
output := os.Stderr
// prep the logger to not clobber the screen from now on (logrus only)
logBuffer := bytes.NewBufferString("")
logWrapper, ok := log.Log.(*logger.LogrusLogger)
if ok {
logWrapper.Logger.SetOutput(logBuffer)
}
// hide cursor
_, _ = fmt.Fprint(output, "\x1b[?25l")
// show cursor
defer fmt.Fprint(output, "\x1b[?25h")
fr := setupScreen(output)
if fr == nil {
return fmt.Errorf("unable to setup screen")
}
var isClosed bool
defer func() {
if !isClosed {
fr.Close()
frame.Close()
// flush any errors to the screen before the report
logWrapper, ok := log.Log.(*logger.LogrusLogger)
if ok {
fmt.Fprint(logWrapper.Output, logBuffer.String())
} else {
fmt.Fprint(output, logBuffer.String())
}
}
logWrapper, ok := log.Log.(*logger.LogrusLogger)
if ok {
logWrapper.Logger.SetOutput(output)
}
}()
var err error
var wg = &sync.WaitGroup{}
events := subscription.Events()
ctx := context.Background()
syftUIHandler := ui.NewHandler()
eventLoop:
for {
select {
case err := <-workerErrs:
// TODO: we should show errors more explicitly in the ETUI
if err != nil {
return err
}
case e, ok := <-events:
if !ok {
break eventLoop
}
switch {
case syftUIHandler.RespondsTo(e):
if err = syftUIHandler.Handle(ctx, fr, e, wg); err != nil {
log.Errorf("unable to show %s event: %+v", e.Type, err)
}
case e.Type == syftEvent.AppUpdateAvailable:
if err = appUpdateAvailableHandler(ctx, fr, e, wg); err != nil {
log.Errorf("unable to show %s event: %+v", e.Type, err)
}
case e.Type == syftEvent.CatalogerFinished:
// we may have other background processes still displaying progress, wait for them to
// finish before discontinuing dynamic content and showing the final report
wg.Wait()
fr.Close()
// TODO: there is a race condition within frame.Close() that sometimes leads to an extra blank line being output
frame.Close()
isClosed = true
// flush any errors to the screen before the report
logWrapper, ok := log.Log.(*logger.LogrusLogger)
if ok {
fmt.Fprint(logWrapper.Output, logBuffer.String())
} else {
fmt.Fprint(output, logBuffer.String())
}
if err := common.CatalogerFinishedHandler(e); err != nil {
log.Errorf("unable to show %s event: %+v", e.Type, err)
}
// this is the last expected event
break eventLoop
}
case <-ctx.Done():
if ctx.Err() != nil {
log.Errorf("cancelled (%+v)", err)
}
break eventLoop
}
}
return nil
}
@@ -0,0 +1,32 @@
package etui
import (
"context"
"fmt"
"io"
"sync"
"github.com/anchore/syft/internal"
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
"github.com/gookit/color"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/jotframe/pkg/frame"
)
// appUpdateAvailableHandler is a UI handler function to display a new application version to the top of the screen.
func appUpdateAvailableHandler(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error {
newVersion, err := syftEventParsers.ParseAppUpdateAvailable(event)
if err != nil {
return fmt.Errorf("bad AppUpdateAvailable event: %w", err)
}
line, err := fr.Prepend()
if err != nil {
return err
}
message := color.Magenta.Sprintf("New version of %s is available: %s", internal.ApplicationName, newVersion)
_, _ = io.WriteString(line, message)
return nil
}
+40
View File
@@ -0,0 +1,40 @@
package ui
import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui/common"
syftEvent "github.com/anchore/syft/syft/event"
"github.com/wagoodman/go-partybus"
)
// LoggerUI is a UI function that leverages the displays all application logs to the screen.
func LoggerUI(workerErrs <-chan error, subscription *partybus.Subscription) error {
events := subscription.Events()
eventLoop:
for {
select {
case err := <-workerErrs:
if err != nil {
return err
}
case e, ok := <-events:
if !ok {
// event bus closed...
break eventLoop
}
// ignore all events except for the final event
if e.Type == syftEvent.CatalogerFinished {
err := common.CatalogerFinishedHandler(e)
if err != nil {
log.Errorf("unable to show catalog image finished event: %+v", err)
}
// this is the last expected event
break eventLoop
}
}
}
return nil
}
+30
View File
@@ -0,0 +1,30 @@
package ui
import (
"os"
"runtime"
"github.com/anchore/syft/internal/ui/etui"
"golang.org/x/crypto/ssh/terminal"
)
// TODO: build tags to exclude options from windows
// Select is responsible for determining the specific UI function given select user option, the current platform
// config values, and environment status (such as a TTY being present).
func Select(verbose, quiet bool) UI {
var ui UI
isStdoutATty := terminal.IsTerminal(int(os.Stdout.Fd()))
isStderrATty := terminal.IsTerminal(int(os.Stderr.Fd()))
notATerminal := !isStderrATty && !isStdoutATty
switch {
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
ui = LoggerUI
default:
ui = etui.OutputToEphemeralTUI
}
return ui
}
+9
View File
@@ -0,0 +1,9 @@
package ui
import (
"github.com/wagoodman/go-partybus"
)
// UI is a function that takes a channel of errors from the main() worker and a event bus subscription and is
// responsible for displaying pertinent events to the user, on the screen or otherwise.
type UI func(<-chan error, *partybus.Subscription) error
+39
View File
@@ -0,0 +1,39 @@
package version
import (
"fmt"
"runtime"
)
const valueNotProvided = "[not provided]"
// all variables here are provided as build-time arguments, with clear default values
var version = valueNotProvided
var gitCommit = valueNotProvided
var gitTreeState = valueNotProvided
var buildDate = valueNotProvided
var platform = fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
// Version defines the application version details (generally from build information)
type Version struct {
Version string // application semantic version
GitCommit string // git SHA at build-time
GitTreeState string // indication of git tree (either "clean" or "dirty") at build-time
BuildDate string // date of the build
GoVersion string // go runtime version at build-time
Compiler string // compiler used at build-time
Platform string // GOOS and GOARCH at build-time
}
// FromBuild provides all version details
func FromBuild() Version {
return Version{
Version: version,
GitCommit: gitCommit,
GitTreeState: gitTreeState,
BuildDate: buildDate,
GoVersion: runtime.Version(),
Compiler: runtime.Compiler,
Platform: platform,
}
}
+72
View File
@@ -0,0 +1,72 @@
package version
import (
"fmt"
"io/ioutil"
"net/http"
"strings"
hashiVersion "github.com/anchore/go-version"
"github.com/anchore/syft/internal"
)
var latestAppVersionURL = struct {
host string
path string
}{
host: "https://toolbox-data.anchore.io",
path: fmt.Sprintf("/%s/releases/latest/VERSION", internal.ApplicationName),
}
func IsUpdateAvailable() (bool, string, error) {
currentVersionStr := FromBuild().Version
currentVersion, err := hashiVersion.NewVersion(currentVersionStr)
if err != nil {
if currentVersionStr == valueNotProvided {
// this is the default build arg and should be ignored (this is not an error case)
return false, "", nil
}
return false, "", fmt.Errorf("failed to parse current application version: %w", err)
}
latestVersion, err := fetchLatestApplicationVersion()
if err != nil {
return false, "", err
}
if latestVersion.GreaterThan(currentVersion) {
return true, latestVersion.String(), nil
}
return false, "", nil
}
func fetchLatestApplicationVersion() (*hashiVersion.Version, error) {
req, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request for latest version: %w", err)
}
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch latest version: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP %d on fetching latest version: %s", resp.StatusCode, resp.Status)
}
versionBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read latest version: %w", err)
}
versionStr := strings.TrimSuffix(string(versionBytes), "\n")
if len(versionStr) > 50 {
return nil, fmt.Errorf("version too long: %q", versionStr[:50])
}
return hashiVersion.NewVersion(versionStr)
}
+209
View File
@@ -0,0 +1,209 @@
package version
import (
"net/http"
"net/http/httptest"
"testing"
hashiVersion "github.com/anchore/go-version"
)
func TestIsUpdateAvailable(t *testing.T) {
tests := []struct {
name string
buildVersion string
latestVersion string
code int
isAvailable bool
newVersion string
err bool
}{
{
name: "equal",
buildVersion: "1.0.0",
latestVersion: "1.0.0",
code: 200,
isAvailable: false,
newVersion: "",
err: false,
},
{
name: "hasUpdate",
buildVersion: "1.0.0",
latestVersion: "1.2.0",
code: 200,
isAvailable: true,
newVersion: "1.2.0",
err: false,
},
{
name: "aheadOfLatest",
buildVersion: "1.2.0",
latestVersion: "1.0.0",
code: 200,
isAvailable: false,
newVersion: "",
err: false,
},
{
name: "EmptyUpdate",
buildVersion: "1.0.0",
latestVersion: "",
code: 200,
isAvailable: false,
newVersion: "",
err: true,
},
{
name: "GarbageUpdate",
buildVersion: "1.0.0",
latestVersion: "hdfjksdhfhkj",
code: 200,
isAvailable: false,
newVersion: "",
err: true,
},
{
name: "BadUpdate",
buildVersion: "1.0.0",
latestVersion: "1.0.",
code: 500,
isAvailable: false,
newVersion: "",
err: true,
},
{
name: "NoBuildVersion",
buildVersion: valueNotProvided,
latestVersion: "1.0.0",
code: 200,
isAvailable: false,
newVersion: "",
err: false,
},
{
name: "BadUpdateValidVersion",
buildVersion: "1.0.0",
latestVersion: "2.0.0",
code: 404,
isAvailable: false,
newVersion: "",
err: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// setup mocks
// local...
version = test.buildVersion
// remote...
handler := http.NewServeMux()
handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(test.code)
_, _ = w.Write([]byte(test.latestVersion))
})
mockSrv := httptest.NewServer(handler)
latestAppVersionURL.host = mockSrv.URL
defer mockSrv.Close()
isAvailable, newVersion, err := IsUpdateAvailable()
if err != nil && !test.err {
t.Fatalf("got error but expected none: %+v", err)
} else if err == nil && test.err {
t.Fatalf("expected error but got none")
}
if newVersion != test.newVersion {
t.Errorf("unexpected NEW version: %+v", newVersion)
}
if isAvailable != test.isAvailable {
t.Errorf("unexpected result: %+v", isAvailable)
}
})
}
}
func TestFetchLatestApplicationVersion(t *testing.T) {
tests := []struct {
name string
response string
code int
err bool
expected *hashiVersion.Version
}{
{
name: "gocase",
response: "1.0.0",
code: 200,
expected: hashiVersion.Must(hashiVersion.NewVersion("1.0.0")),
},
{
name: "garbage",
response: "garbage",
code: 200,
expected: nil,
err: true,
},
{
name: "http 500",
response: "1.0.0",
code: 500,
expected: nil,
err: true,
},
{
name: "http 404",
response: "1.0.0",
code: 404,
expected: nil,
err: true,
},
{
name: "empty",
response: "",
code: 200,
expected: nil,
err: true,
},
{
name: "too long",
response: "this is really long this is really long this is really long this is really long this is really long this is really long this is really long this is really long ",
code: 200,
expected: nil,
err: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// setup mock
handler := http.NewServeMux()
handler.HandleFunc(latestAppVersionURL.path, func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(test.code)
_, _ = w.Write([]byte(test.response))
})
mockSrv := httptest.NewServer(handler)
latestAppVersionURL.host = mockSrv.URL
defer mockSrv.Close()
actual, err := fetchLatestApplicationVersion()
if err != nil && !test.err {
t.Fatalf("got error but expected none: %+v", err)
} else if err == nil && test.err {
t.Fatalf("expected error but got none")
}
if err != nil {
return
}
if actual.String() != test.expected.String() {
t.Errorf("unexpected version: %+v", actual.String())
}
})
}
}
+6 -13
View File
@@ -1,21 +1,14 @@
/*
Syft is a CLI tool and go library for generating a Software Bill of Materials (SBOM) from container images and filesystems.
Note that Syft is both a command line tool as well as a library. See the syft/ child package for library functionality.
*/
package main
import (
"github.com/anchore/imgbom/cmd"
)
var (
version = "No version provided"
commit = "No commit provided"
buildTime = "No build timestamp provided"
"github.com/anchore/syft/cmd"
)
func main() {
cmd.SetVersion(&cmd.Version{
Version: version,
Commit: commit,
BuildTime: buildTime,
})
cmd.Execute()
}
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Source this file in your shell sessions in order to use this shell completion.
complete -W "$(docker images --format '{{.Repository}}:{{.Tag}}')" syft
+1
View File
@@ -0,0 +1 @@
bom.xml
+5
View File
@@ -0,0 +1,5 @@
.DEFAULT_GOAL := validate-schema
.PHONY: validate-schema
validate-schema:
go run ../../main.go ubuntu:latest -vv -o cyclonedx > bom.xml
xmllint --noout --schema ./cyclonedx.xsd bom.xml
+7
View File
@@ -0,0 +1,7 @@
# CycloneDX Schemas
`syft` generates a CycloneDX BOm output. We want to be able to validate the CycloneDX schemas
(and dependent schemas) against generated syft output. The best way to do this is with `xmllint`,
however, this tool does not know how to deal with references from HTTP, only the local filesystem.
For this reason we've included a copy of all schemas needed to validate `syft` output, modified
to reference local copies of dependent schemas.
+183
View File
@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
CycloneDX BOM Descriptor Extension
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:vc="http://www.w3.org/2007/XMLSchema-versioning"
xmlns:bom="http://cyclonedx.org/schema/bom/1.1"
xmlns:bd="http://cyclonedx.org/schema/ext/bom-descriptor/1.0"
elementFormDefault="qualified"
targetNamespace="http://cyclonedx.org/schema/ext/bom-descriptor/1.0"
vc:minVersion="1.0"
vc:maxVersion="1.1"
version="1.0">
<xs:annotation>
<xs:documentation>
<name>CycloneDX BOM Descriptor Extension</name>
<url>https://cyclonedx.org/ext/bom-descriptor</url>
<license uri="http://www.apache.org/licenses/LICENSE-2.0"
version="2.0">Apache License, Version 2.0</license>
<authors>
<author>Steve Springett</author>
</authors>
</xs:documentation>
</xs:annotation>
<xs:import namespace="http://cyclonedx.org/schema/bom/1.1" schemaLocation="http://cyclonedx.org/schema/bom/1.1"/>
<xs:complexType name="metadata">
<xs:sequence minOccurs="0" maxOccurs="1">
<xs:element name="timestamp" type="xs:dateTime" minOccurs="0">
<xs:annotation>
<xs:documentation>The date and time (timestamp) when the document was created.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="tool" minOccurs="0" type="bd:toolType">
<xs:annotation>
<xs:documentation>The tool used to create the BOM.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="authors" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The person(s) who created the BOM. Authors are common in BOMs created through
manual processes. BOMs created through automated means may not have authors.</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="author" type="bd:organizationalPerson"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:element name="component" type="bom:component" minOccurs="0">
<xs:annotation>
<xs:documentation>The component that the BOM describes.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="manufacture" type="bd:organizationalEntity" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>The organization that manufactured the component that the BOM describes.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="supplier" type="bd:organizationalEntity" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>The organization that supplied the component that the BOM describes. The
supplier may often be the manufacture, but may also be a distributor or repackager.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:anyAttribute namespace="##other" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
<xs:complexType name="organizationalEntity">
<xs:sequence minOccurs="0" maxOccurs="1">
<xs:element name="name" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The name of the organization</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="url" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>The URL of the organization. Multiple URLs are allowed.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="contact" type="bd:organizationalPerson" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>A contact person at the organization. Multiple contacts are allowed.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:anyAttribute namespace="##other" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
<xs:complexType name="toolType">
<xs:annotation>
<xs:documentation>Specifies a tool (manual or automated).</xs:documentation>
</xs:annotation>
<xs:sequence minOccurs="0" maxOccurs="1">
<xs:element name="vendor" minOccurs="0" maxOccurs="1" type="xs:normalizedString">
<xs:annotation>
<xs:documentation>The vendor of the tool used to create the BOM.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="name" minOccurs="0" maxOccurs="1" type="xs:normalizedString">
<xs:annotation>
<xs:documentation>The name of the tool used to create the BOM.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="version" minOccurs="0" maxOccurs="1" type="xs:normalizedString">
<xs:annotation>
<xs:documentation>The version of the tool used to create the BOM.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="hashes" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
<xs:element name="hash" type="bom:hashType"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:sequence>
<xs:anyAttribute namespace="##other" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
<xs:complexType name="organizationalPerson">
<xs:sequence minOccurs="0" maxOccurs="1">
<xs:element name="name" type="xs:normalizedString" minOccurs="0" maxOccurs="1">
<xs:annotation>
<xs:documentation>The name of the person</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="email" type="xs:normalizedString" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>The email address of the person. Multiple email addresses are allowed.</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="phone" type="xs:normalizedString" minOccurs="0" maxOccurs="unbounded">
<xs:annotation>
<xs:documentation>The phone number of the person. Multiple phone numbers are allowed.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
<xs:anyAttribute namespace="##other" processContents="lax">
<xs:annotation>
<xs:documentation>User-defined attributes may be used on this element as long as they
do not have the same name as an existing attribute used by the schema.</xs:documentation>
</xs:annotation>
</xs:anyAttribute>
</xs:complexType>
<xs:element name="metadata" type="bd:metadata">
<xs:annotation>
<xs:documentation>Provides additional information about a BOM.</xs:documentation>
</xs:annotation>
</xs:element>
</xs:schema>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
examples/
+30
View File
@@ -0,0 +1,30 @@
#!/usr/env/bin python3
import os
import glob
import json
from genson import SchemaBuilder
EXAMPLES_DIR = "examples/"
OUTPUT = "schema.json"
def main():
builder = SchemaBuilder()
print("Generating new Syft json schema...")
for filepath in glob.glob(os.path.join(EXAMPLES_DIR, '*.json')):
with open(filepath, 'r') as f:
print(f" adding {filepath}")
builder.add_object(json.loads(f.read()))
print("Building schema...")
new_schema = builder.to_schema()
with open(OUTPUT, 'w') as f:
f.write(json.dumps(new_schema, sort_keys=True, indent=4))
print(f"New schema written to '{OUTPUT}'")
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
genson
+534
View File
@@ -0,0 +1,534 @@
{
"$schema": "http://json-schema.org/schema#",
"properties": {
"artifacts": {
"items": {
"properties": {
"found-by": {
"items": {
"type": "string"
},
"type": "array"
},
"locations": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"properties": {
"layer-index": {
"type": "integer"
},
"path": {
"type": "string"
}
},
"required": [
"layer-index",
"path"
],
"type": "object"
}
]
},
"type": "array"
},
"metadata": {
"properties": {
"architecture": {
"type": "string"
},
"description": {
"type": "string"
},
"epoch": {
"type": "integer"
},
"files": {
"items": {
"properties": {
"checksum": {
"type": "string"
},
"owner-gid": {
"type": "string"
},
"owner-uid": {
"type": "string"
},
"path": {
"type": "string"
},
"permissions": {
"type": "string"
}
},
"required": [
"checksum",
"owner-gid",
"owner-uid",
"path",
"permissions"
],
"type": "object"
},
"type": "array"
},
"git-commit-of-apk-port": {
"type": "string"
},
"installed-size": {
"type": "integer"
},
"license": {
"type": "string"
},
"maintainer": {
"type": "string"
},
"manifest": {
"anyOf": [
{
"type": "null"
},
{
"properties": {
"extra-fields": {
"properties": {
"Archiver-Version": {
"type": "string"
},
"Build-Jdk": {
"type": "string"
},
"Built-By": {
"type": "string"
},
"Created-By": {
"type": "string"
},
"Extension-Name": {
"type": "string"
},
"Group-Id": {
"type": "string"
},
"Hudson-Version": {
"type": "string"
},
"Jenkins-Version": {
"type": "string"
},
"Long-Name": {
"type": "string"
},
"Main-Class": {
"type": "string"
},
"Minimum-Java-Version": {
"type": "string"
},
"Plugin-Dependencies": {
"type": "string"
},
"Plugin-Developers": {
"type": "string"
},
"Plugin-License-Name": {
"type": "string"
},
"Plugin-License-Url": {
"type": "string"
},
"Plugin-ScmUrl": {
"type": "string"
},
"Plugin-Version": {
"type": "string"
},
"Short-Name": {
"type": "string"
}
},
"required": [
"Archiver-Version",
"Build-Jdk",
"Built-By",
"Created-By"
],
"type": "object"
},
"implementation-title": {
"type": "string"
},
"implementation-vendor": {
"type": "string"
},
"implementation-version": {
"type": "string"
},
"manifest-version": {
"type": "string"
},
"name": {
"type": "string"
},
"specification-title": {
"type": "string"
},
"specification-vendor": {
"type": "string"
},
"specification-version": {
"type": "string"
}
},
"required": [
"extra-fields",
"implementation-title",
"implementation-vendor",
"implementation-version",
"manifest-version",
"name",
"specification-title",
"specification-vendor",
"specification-version"
],
"type": "object"
}
]
},
"origin-package": {
"type": "string"
},
"package": {
"type": "string"
},
"parent-package": {
"anyOf": [
{
"type": "null"
},
{
"properties": {
"found-by": {
"type": "string"
},
"language": {
"type": "integer"
},
"licenses": {
"type": "null"
},
"manifest": {
"type": "string"
},
"metadata": {
"properties": {
"manifest": {
"properties": {
"extra-fields": {
"properties": {
"Archiver-Version": {
"type": "string"
},
"Build-Jdk": {
"type": "string"
},
"Built-By": {
"type": "string"
},
"Created-By": {
"type": "string"
},
"Extension-Name": {
"type": "string"
},
"Group-Id": {
"type": "string"
},
"Hudson-Version": {
"type": "string"
},
"Jenkins-Version": {
"type": "string"
},
"Long-Name": {
"type": "string"
},
"Main-Class": {
"type": "string"
},
"Minimum-Java-Version": {
"type": "string"
},
"Plugin-Dependencies": {
"type": "string"
},
"Plugin-Developers": {
"type": "string"
},
"Plugin-License-Name": {
"type": "string"
},
"Plugin-License-Url": {
"type": "string"
},
"Plugin-ScmUrl": {
"type": "string"
},
"Plugin-Version": {
"type": "string"
},
"Short-Name": {
"type": "string"
}
},
"required": [
"Archiver-Version",
"Build-Jdk",
"Built-By",
"Created-By"
],
"type": "object"
},
"implementation-title": {
"type": "string"
},
"implementation-vendor": {
"type": "string"
},
"implementation-version": {
"type": "string"
},
"manifest-version": {
"type": "string"
},
"name": {
"type": "string"
},
"specification-title": {
"type": "string"
},
"specification-vendor": {
"type": "string"
},
"specification-version": {
"type": "string"
}
},
"required": [
"extra-fields",
"implementation-title",
"implementation-vendor",
"implementation-version",
"manifest-version",
"name",
"specification-title",
"specification-vendor",
"specification-version"
],
"type": "object"
},
"parent-package": {
"type": "null"
},
"pom-properties": {
"properties": {
"Path": {
"type": "string"
},
"artifact-id": {
"type": "string"
},
"extra-fields": {
"type": "null"
},
"group-id": {
"type": "string"
},
"name": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"Path",
"artifact-id",
"extra-fields",
"group-id",
"name",
"version"
],
"type": "object"
}
},
"required": [
"manifest",
"parent-package",
"pom-properties"
],
"type": "object"
},
"sources": {
"type": "null"
},
"type": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"found-by",
"language",
"licenses",
"manifest",
"metadata",
"sources",
"type",
"version"
],
"type": "object"
}
]
},
"pom-properties": {
"properties": {
"Path": {
"type": "string"
},
"artifact-id": {
"type": "string"
},
"extra-fields": {
"type": "null"
},
"group-id": {
"type": "string"
},
"name": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"Path",
"artifact-id",
"extra-fields",
"group-id",
"name",
"version"
],
"type": "object"
},
"pull-checksum": {
"type": "string"
},
"pull-dependencies": {
"type": "string"
},
"release": {
"type": "string"
},
"size": {
"type": "integer"
},
"source": {
"type": "string"
},
"url": {
"type": "string"
},
"version": {
"type": "string"
}
},
"type": "object"
},
"name": {
"type": "string"
},
"type": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"found-by",
"locations",
"name",
"type",
"version"
],
"type": "object"
},
"type": "array"
},
"directory": {
"type": "string"
},
"image": {
"properties": {
"digest": {
"type": "string"
},
"layers": {
"items": {
"properties": {
"digest": {
"type": "string"
},
"media-type": {
"type": "string"
},
"size": {
"type": "integer"
}
},
"required": [
"digest",
"media-type",
"size"
],
"type": "object"
},
"type": "array"
},
"media-type": {
"type": "string"
},
"size": {
"type": "integer"
},
"tags": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"digest",
"layers",
"media-type",
"size",
"tags"
],
"type": "object"
}
},
"required": [
"artifacts"
],
"type": "object"
}
+44
View File
@@ -0,0 +1,44 @@
/*
Package apkdb provides a concrete Cataloger implementation for Alpine DB files.
*/
package apkdb
import (
"io"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/scope"
)
// Cataloger catalogs pkg.ApkPkg Package Types defined in Alpine DB files.
type Cataloger struct {
cataloger common.GenericCataloger
}
// New returns a new Alpine DB cataloger object.
func New() *Cataloger {
globParsers := map[string]common.ParserFn{
"**/lib/apk/db/installed": parseApkDB,
}
return &Cataloger{
cataloger: common.NewGenericCataloger(nil, globParsers),
}
}
// Name returns a string that uniquely describes this cataloger.
func (c *Cataloger) Name() string {
return "apkdb-cataloger"
}
// SelectFiles returns a set of discovered Alpine DB files from the user content source.
func (c *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return c.cataloger.SelectFiles(resolver)
}
// Catalog returns the Packages indexed from all Alpine DB files discovered.
func (c *Cataloger) Catalog(contents map[file.Reference]io.Reader) ([]pkg.Package, error) {
return c.cataloger.Catalog(contents)
}
+135
View File
@@ -0,0 +1,135 @@
package apkdb
import (
"bufio"
"fmt"
"io"
"path"
"strconv"
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg"
"github.com/mitchellh/mapstructure"
)
// integrity check
var _ common.ParserFn = parseApkDB
// parseApkDb parses individual packages from a given Alpine DB file. For more information on specific fields
// see https://wiki.alpinelinux.org/wiki/Apk_spec .
func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, error) {
packages := make([]pkg.Package, 0)
scanner := bufio.NewScanner(reader)
onDoubleLF := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
for i := 0; i < len(data); i++ {
if i > 0 && data[i-1] == '\n' && data[i] == '\n' {
return i + 1, data[:i-1], nil
}
}
if !atEOF {
return 0, nil, nil
}
// deliver the last token (which could be an empty string)
return 0, data, bufio.ErrFinalToken
}
scanner.Split(onDoubleLF)
for scanner.Scan() {
metadata, err := parseApkDBEntry(strings.NewReader(scanner.Text()))
if err != nil {
return nil, err
}
if metadata != nil {
packages = append(packages, pkg.Package{
Name: metadata.Package,
Version: metadata.Version,
Licenses: strings.Split(metadata.License, " "),
Type: pkg.ApkPkg,
Metadata: *metadata,
})
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to parse APK DB file: %w", err)
}
return packages, nil
}
// nolint:funlen
// parseApkDBEntry reads and parses a single pkg.ApkMetadata element from the stream, returning nil if their are no more entries.
func parseApkDBEntry(reader io.Reader) (*pkg.ApkMetadata, error) {
var entry pkg.ApkMetadata
pkgFields := make(map[string]interface{})
files := make([]pkg.ApkFileRecord, 0)
var fileRecord *pkg.ApkFileRecord
lastFile := "/"
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
fields := strings.SplitN(line, ":", 2)
if len(fields) != 2 {
continue
}
key := fields[0]
value := strings.TrimSpace(fields[1])
switch key {
case "F":
lastFile = "/" + value
continue
case "R":
newFileRecord := pkg.ApkFileRecord{
Path: path.Join(lastFile, value),
}
files = append(files, newFileRecord)
fileRecord = &files[len(files)-1]
case "a":
ownershipFields := strings.Split(value, ":")
if len(ownershipFields) != 3 {
log.Errorf("unexpected APK ownership field: %q", value)
continue
}
if fileRecord == nil {
log.Errorf("ownership field with no parent record: %q", value)
continue
}
fileRecord.OwnerUID = ownershipFields[0]
fileRecord.OwnerGUI = ownershipFields[1]
fileRecord.Permissions = ownershipFields[2]
case "Z":
if fileRecord == nil {
log.Errorf("checksum field with no parent record: %q", value)
continue
}
fileRecord.Checksum = value
case "I", "S":
// coerce to integer
iVal, err := strconv.Atoi(value)
if err != nil {
return nil, fmt.Errorf("failed to parse APK int: '%+v'", value)
}
pkgFields[key] = iVal
default:
pkgFields[key] = value
}
}
if err := mapstructure.Decode(pkgFields, &entry); err != nil {
return nil, fmt.Errorf("unable to parse APK metadata: %w", err)
}
if entry.Package == "" {
return nil, nil
}
entry.Files = files
return &entry, nil
}
+228
View File
@@ -0,0 +1,228 @@
package apkdb
import (
"bufio"
"os"
"testing"
"github.com/go-test/deep"
"github.com/anchore/syft/syft/pkg"
)
func TestSinglePackage(t *testing.T) {
tests := []struct {
name string
expected pkg.ApkMetadata
}{
{
name: "Test Single Package",
expected: pkg.ApkMetadata{
Package: "musl-utils",
OriginPackage: "musl",
Version: "1.1.24-r2",
Description: "the musl c library (libc) implementation",
Maintainer: "Timo Teräs <timo.teras@iki.fi>",
License: "MIT BSD GPL2+",
Architecture: "x86_64",
URL: "https://musl.libc.org/",
Size: 37944,
InstalledSize: 151552,
PullDependencies: "scanelf so:libc.musl-x86_64.so.1",
PullChecksum: "Q1bTtF5526tETKfL+lnigzIDvm+2o=",
GitCommitOfAport: "4024cc3b29ad4c65544ad068b8f59172b5494306",
Files: []pkg.ApkFileRecord{
{
Path: "/sbin/ldconfig",
OwnerUID: "0",
OwnerGUI: "0",
Permissions: "755",
Checksum: "Q1Kja2+POZKxEkUOZqwSjC6kmaED4=",
},
{
Path: "/usr/bin/iconv",
OwnerUID: "0",
OwnerGUI: "0",
Permissions: "755",
Checksum: "Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY=",
},
{
Path: "/usr/bin/ldd",
OwnerUID: "0",
OwnerGUI: "0",
Permissions: "755",
Checksum: "Q1yFAhGggmL7ERgbIA7KQxyTzf3ks=",
},
{
Path: "/usr/bin/getconf",
OwnerUID: "0",
OwnerGUI: "0",
Permissions: "755",
Checksum: "Q1dAdYK8M/INibRQF5B3Rw7cmNDDA=",
},
{
Path: "/usr/bin/getent",
OwnerUID: "0",
OwnerGUI: "0",
Permissions: "755",
Checksum: "Q1eR2Dz/WylabgbWMTkd2+hGmEya4=",
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
file, err := os.Open("test-fixtures/single")
if err != nil {
t.Fatal("Unable to read test_fixtures/single: ", err)
}
defer func() {
err := file.Close()
if err != nil {
t.Fatal("closing file failed:", err)
}
}()
reader := bufio.NewReader(file)
entry, err := parseApkDBEntry(reader)
if err != nil {
t.Fatal("Unable to read file contents: ", err)
}
if diff := deep.Equal(*entry, test.expected); diff != nil {
for _, d := range diff {
t.Errorf("diff: %+v", d)
}
}
})
}
}
func TestMultiplePackages(t *testing.T) {
tests := []struct {
fixture string
expected []pkg.Package
}{
{
fixture: "test-fixtures/multiple",
expected: []pkg.Package{
{
Name: "libc-utils",
Version: "0.7.2-r0",
Licenses: []string{"BSD"},
Type: pkg.ApkPkg,
Metadata: pkg.ApkMetadata{
Package: "libc-utils",
OriginPackage: "libc-dev",
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
Version: "0.7.2-r0",
License: "BSD",
Architecture: "x86_64",
URL: "http://alpinelinux.org",
Description: "Meta package to pull in correct libc",
Size: 1175,
InstalledSize: 4096,
PullChecksum: "Q1p78yvTLG094tHE1+dToJGbmYzQE=",
GitCommitOfAport: "97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479",
PullDependencies: "musl-utils",
Files: []pkg.ApkFileRecord{},
},
},
{
Name: "musl-utils",
Version: "1.1.24-r2",
Licenses: []string{"MIT", "BSD", "GPL2+"},
Type: pkg.ApkPkg,
Metadata: pkg.ApkMetadata{
Package: "musl-utils",
OriginPackage: "musl",
Version: "1.1.24-r2",
Description: "the musl c library (libc) implementation",
Maintainer: "Timo Teräs <timo.teras@iki.fi>",
License: "MIT BSD GPL2+",
Architecture: "x86_64",
URL: "https://musl.libc.org/",
Size: 37944,
InstalledSize: 151552,
PullDependencies: "scanelf so:libc.musl-x86_64.so.1",
PullChecksum: "Q1bTtF5526tETKfL+lnigzIDvm+2o=",
GitCommitOfAport: "4024cc3b29ad4c65544ad068b8f59172b5494306",
Files: []pkg.ApkFileRecord{
{
Path: "/sbin/ldconfig",
OwnerUID: "0",
OwnerGUI: "0",
Permissions: "755",
Checksum: "Q1Kja2+POZKxEkUOZqwSjC6kmaED4=",
},
{
Path: "/usr/bin/iconv",
OwnerUID: "0",
OwnerGUI: "0",
Permissions: "755",
Checksum: "Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY=",
},
{
Path: "/usr/bin/ldd",
OwnerUID: "0",
OwnerGUI: "0",
Permissions: "755",
Checksum: "Q1yFAhGggmL7ERgbIA7KQxyTzf3ks=",
},
{
Path: "/usr/bin/getconf",
OwnerUID: "0",
OwnerGUI: "0",
Permissions: "755",
Checksum: "Q1dAdYK8M/INibRQF5B3Rw7cmNDDA=",
},
{
Path: "/usr/bin/getent",
OwnerUID: "0",
OwnerGUI: "0",
Permissions: "755",
Checksum: "Q1eR2Dz/WylabgbWMTkd2+hGmEya4=",
},
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
file, err := os.Open(test.fixture)
if err != nil {
t.Fatal("Unable to read: ", err)
}
defer func() {
err := file.Close()
if err != nil {
t.Fatal("closing file failed:", err)
}
}()
pkgs, err := parseApkDB(file.Name(), file)
if err != nil {
t.Fatal("Unable to read file contents: ", err)
}
if len(pkgs) != 2 {
t.Fatalf("unexpected number of entries: %d", len(pkgs))
}
for idx, entry := range pkgs {
if diff := deep.Equal(entry, test.expected[idx]); diff != nil {
for _, d := range diff {
t.Errorf("diff: %+v", d)
}
}
}
})
}
}
@@ -0,0 +1,56 @@
C:Q1p78yvTLG094tHE1+dToJGbmYzQE=
P:libc-utils
V:0.7.2-r0
A:x86_64
S:1175
I:4096
T:Meta package to pull in correct libc
U:http://alpinelinux.org
L:BSD
o:libc-dev
m:Natanael Copa <ncopa@alpinelinux.org>
t:1575749004
c:97b1c2842faa3bfa30f5811ffbf16d5ff9f1a479
D:musl-utils
C:Q1bTtF5526tETKfL+lnigzIDvm+2o=
P:musl-utils
V:1.1.24-r2
A:x86_64
S:37944
I:151552
T:the musl c library (libc) implementation
U:https://musl.libc.org/
L:MIT BSD GPL2+
o:musl
m:Timo Teräs <timo.teras@iki.fi>
t:1584790550
c:4024cc3b29ad4c65544ad068b8f59172b5494306
D:scanelf so:libc.musl-x86_64.so.1
p:cmd:getconf cmd:getent cmd:iconv cmd:ldconfig cmd:ldd
r:libiconv
F:sbin
R:ldconfig
a:0:0:755
Z:Q1Kja2+POZKxEkUOZqwSjC6kmaED4=
F:usr
F:usr/bin
R:iconv
a:0:0:755
Z:Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY=
R:ldd
a:0:0:755
Z:Q1yFAhGggmL7ERgbIA7KQxyTzf3ks=
R:getconf
a:0:0:755
Z:Q1dAdYK8M/INibRQF5B3Rw7cmNDDA=
R:getent
a:0:0:755
Z:Q1eR2Dz/WylabgbWMTkd2+hGmEya4=
+34
View File
@@ -0,0 +1,34 @@
C:Q1bTtF5526tETKfL+lnigzIDvm+2o=
P:musl-utils
V:1.1.24-r2
A:x86_64
S:37944
I:151552
T:the musl c library (libc) implementation
U:https://musl.libc.org/
L:MIT BSD GPL2+
o:musl
m:Timo Teräs <timo.teras@iki.fi>
t:1584790550
c:4024cc3b29ad4c65544ad068b8f59172b5494306
D:scanelf so:libc.musl-x86_64.so.1
p:cmd:getconf cmd:getent cmd:iconv cmd:ldconfig cmd:ldd
r:libiconv
F:sbin
R:ldconfig
a:0:0:755
Z:Q1Kja2+POZKxEkUOZqwSjC6kmaED4=
F:usr
F:usr/bin
R:iconv
a:0:0:755
Z:Q1CVmFbdY+Hv6/jAHl1gec2Kbx1EY=
R:ldd
a:0:0:755
Z:Q1yFAhGggmL7ERgbIA7KQxyTzf3ks=
R:getconf
a:0:0:755
Z:Q1dAdYK8M/INibRQF5B3Rw7cmNDDA=
R:getent
a:0:0:755
Z:Q1eR2Dz/WylabgbWMTkd2+hGmEya4=
+44
View File
@@ -0,0 +1,44 @@
/*
Package bundler provides a concrete Cataloger implementation for Ruby Gemfile.lock bundler files.
*/
package bundler
import (
"io"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/scope"
)
// Cataloger catalogs pkg.GemPkg Package Types defined in Bundler Gemfile.lock files.
type Cataloger struct {
cataloger common.GenericCataloger
}
// New returns a new Bundler cataloger object.
func New() *Cataloger {
globParsers := map[string]common.ParserFn{
"**/Gemfile.lock": parseGemfileLockEntries,
}
return &Cataloger{
cataloger: common.NewGenericCataloger(nil, globParsers),
}
}
// Name returns a string that uniquely describes this cataloger.
func (c *Cataloger) Name() string {
return "bundler-cataloger"
}
// SelectFiles returns a set of discovered Gemfile.lock files from the user content source.
func (c *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return c.cataloger.SelectFiles(resolver)
}
// Catalog returns the Packages indexed from all Gemfile.lock files discovered.
func (c *Cataloger) Catalog(contents map[file.Reference]io.Reader) ([]pkg.Package, error) {
return c.cataloger.Catalog(contents)
}
@@ -0,0 +1,62 @@
package bundler
import (
"bufio"
"io"
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg"
)
// integrity check
var _ common.ParserFn = parseGemfileLockEntries
var sectionsOfInterest = internal.NewStringSetFromSlice([]string{"GEM"})
// parseGemfileLockEntries is a parser function for Gemfile.lock contents, returning all Gems discovered.
func parseGemfileLockEntries(_ string, reader io.Reader) ([]pkg.Package, error) {
pkgs := make([]pkg.Package, 0)
scanner := bufio.NewScanner(reader)
var currentSection string
for scanner.Scan() {
line := scanner.Text()
sanitizedLine := strings.TrimSpace(line)
if len(line) > 1 && line[0] != ' ' {
// start of section
currentSection = sanitizedLine
continue
} else if !sectionsOfInterest.Contains(currentSection) {
// skip this line, we're in the wrong section
continue
}
if isDependencyLine(line) {
candidate := strings.Fields(sanitizedLine)
if len(candidate) != 2 {
continue
}
pkgs = append(pkgs, pkg.Package{
Name: candidate[0],
Version: strings.Trim(candidate[1], "()"),
Language: pkg.Ruby,
Type: pkg.BundlerPkg,
})
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return pkgs, nil
}
func isDependencyLine(line string) bool {
if len(line) < 5 {
return false
}
return strings.Count(line[:5], " ") == 4
}
@@ -0,0 +1,100 @@
package bundler
import (
"os"
"testing"
"github.com/anchore/syft/syft/pkg"
)
var expected = map[string]string{
"actionmailer": "4.1.1",
"actionpack": "4.1.1",
"actionview": "4.1.1",
"activemodel": "4.1.1",
"activerecord": "4.1.1",
"activesupport": "4.1.1",
"arel": "5.0.1.20140414130214",
"bootstrap-sass": "3.1.1.1",
"builder": "3.2.2",
"coffee-rails": "4.0.1",
"coffee-script": "2.2.0",
"coffee-script-source": "1.7.0",
"erubis": "2.7.0",
"execjs": "2.0.2",
"hike": "1.2.3",
"i18n": "0.6.9",
"jbuilder": "2.0.7",
"jquery-rails": "3.1.0",
"json": "1.8.1",
"kgio": "2.9.2",
"libv8": "3.16.14.3",
"mail": "2.5.4",
"mime-types": "1.25.1",
"minitest": "5.3.4",
"multi_json": "1.10.1",
"mysql2": "0.3.16",
"polyglot": "0.3.4",
"rack": "1.5.2",
"rack-test": "0.6.2",
"rails": "4.1.1",
"railties": "4.1.1",
"raindrops": "0.13.0",
"rake": "10.3.2",
"rdoc": "4.1.1",
"ref": "1.0.5",
"sass": "3.2.19",
"sass-rails": "4.0.3",
"sdoc": "0.4.0",
"spring": "1.1.3",
"sprockets": "2.11.0",
"sprockets-rails": "2.1.3",
"sqlite3": "1.3.9",
"therubyracer": "0.12.1",
"thor": "0.19.1",
"thread_safe": "0.3.3",
"tilt": "1.4.1",
"treetop": "1.4.15",
"turbolinks": "2.2.2",
"tzinfo": "1.2.0",
"uglifier": "2.5.0",
"unicorn": "4.8.3",
}
func TestParseGemfileLockEntries(t *testing.T) {
fixture, err := os.Open("test-fixtures/Gemfile.lock")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseGemfileLockEntries(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse gemfile lock: %+v", err)
}
if len(actual) != len(expected) {
for _, a := range actual {
t.Log(" ", a)
}
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(expected))
}
for _, a := range actual {
expectedVersion, ok := expected[a.Name]
if !ok {
t.Errorf("unexpected package found: %s", a.Name)
}
if expectedVersion != a.Version {
t.Errorf("unexpected package version (pkg=%s): %s", a.Name, a.Version)
}
if a.Language != pkg.Ruby {
t.Errorf("bad language (pkg=%+v): %+v", a.Name, a.Language)
}
if a.Type != pkg.BundlerPkg {
t.Errorf("bad package type (pkg=%+v): %+v", a.Name, a.Type)
}
}
}
@@ -0,0 +1,149 @@
GEM
remote: https://rubygems.org/
specs:
actionmailer (4.1.1)
actionpack (= 4.1.1)
actionview (= 4.1.1)
mail (~> 2.5.4)
actionpack (4.1.1)
actionview (= 4.1.1)
activesupport (= 4.1.1)
rack (~> 1.5.2)
rack-test (~> 0.6.2)
actionview (4.1.1)
activesupport (= 4.1.1)
builder (~> 3.1)
erubis (~> 2.7.0)
activemodel (4.1.1)
activesupport (= 4.1.1)
builder (~> 3.1)
activerecord (4.1.1)
activemodel (= 4.1.1)
activesupport (= 4.1.1)
arel (~> 5.0.0)
activesupport (4.1.1)
i18n (~> 0.6, >= 0.6.9)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.1)
tzinfo (~> 1.1)
arel (5.0.1.20140414130214)
bootstrap-sass (3.1.1.1)
sass (~> 3.2)
builder (3.2.2)
coffee-rails (4.0.1)
coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.0)
coffee-script (2.2.0)
coffee-script-source
execjs
coffee-script-source (1.7.0)
erubis (2.7.0)
execjs (2.0.2)
hike (1.2.3)
i18n (0.6.9)
jbuilder (2.0.7)
activesupport (>= 3.0.0, < 5)
multi_json (~> 1.2)
jquery-rails (3.1.0)
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
json (1.8.1)
kgio (2.9.2)
libv8 (3.16.14.3)
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.25.1)
minitest (5.3.4)
multi_json (1.10.1)
mysql2 (0.3.16)
polyglot (0.3.4)
rack (1.5.2)
rack-test (0.6.2)
rack (>= 1.0)
rails (4.1.1)
actionmailer (= 4.1.1)
actionpack (= 4.1.1)
actionview (= 4.1.1)
activemodel (= 4.1.1)
activerecord (= 4.1.1)
activesupport (= 4.1.1)
bundler (>= 1.3.0, < 2.0)
railties (= 4.1.1)
sprockets-rails (~> 2.0)
railties (4.1.1)
actionpack (= 4.1.1)
activesupport (= 4.1.1)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
raindrops (0.13.0)
rake (10.3.2)
rdoc (4.1.1)
json (~> 1.4)
ref (1.0.5)
sass (3.2.19)
sass-rails (4.0.3)
railties (>= 4.0.0, < 5.0)
sass (~> 3.2.0)
sprockets (~> 2.8, <= 2.11.0)
sprockets-rails (~> 2.0)
sdoc (0.4.0)
json (~> 1.8)
rdoc (~> 4.0, < 5.0)
spring (1.1.3)
sprockets (2.11.0)
hike (~> 1.2)
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
sprockets-rails (2.1.3)
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (~> 2.8)
sqlite3 (1.3.9)
therubyracer (0.12.1)
libv8 (~> 3.16.14.0)
ref
thor (0.19.1)
thread_safe (0.3.3)
tilt (1.4.1)
treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
turbolinks (2.2.2)
coffee-rails
tzinfo (1.2.0)
thread_safe (~> 0.1)
uglifier (2.5.0)
execjs (>= 0.3.0)
json (>= 1.8.0)
unicorn (4.8.3)
kgio (~> 2.6)
rack
raindrops (~> 0.7)
BAD-SECTION
bad-entry (5.5.5)
bad-kgio (~> 2.6)
bad-rack
bad-raindrops (~> 0.7)
PLATFORMS
ruby
DEPENDENCIES
bootstrap-sass
coffee-rails (~> 4.0.0)
jbuilder (~> 2.0)
jquery-rails
mysql2 (~> 0.3.16)
rails (= 4.1.1)
sass-rails (~> 4.0.3)
sdoc (~> 0.4.0)
spring
sqlite3
therubyracer
turbolinks
uglifier (>= 1.3.0)
unicorn
+89
View File
@@ -0,0 +1,89 @@
package cataloger
import (
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/scope"
"github.com/hashicorp/go-multierror"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
)
// Catalog a given scope (container image or filesystem) with the given catalogers, returning all discovered packages.
// In order to efficiently retrieve contents from a underlying container image the content fetch requests are
// done in bulk. Specifically, all files of interest are collected from each catalogers and accumulated into a single
// request.
func Catalog(resolver scope.Resolver, catalogers ...Cataloger) (*pkg.Catalog, error) {
catalog := pkg.NewCatalog()
fileSelection := make([]file.Reference, 0)
filesProcessed, packagesDiscovered := newMonitor()
// ask catalogers for files to extract from the image tar
for _, a := range catalogers {
catalogerSelection := a.SelectFiles(resolver)
fileSelection = append(fileSelection, catalogerSelection...)
log.Debugf("cataloger '%s' selected '%d' files", a.Name(), len(catalogerSelection))
filesProcessed.N += int64(len(fileSelection))
}
// fetch contents for requested selection by catalogers
// TODO: we should consider refactoring to return a set of io.Readers instead of the full contents themselves (allow for optional buffering).
contents, err := resolver.MultipleFileContentsByRef(fileSelection...)
if err != nil {
return nil, err
}
// perform analysis, accumulating errors for each failed analysis
var errs error
for _, c := range catalogers {
// TODO: check for multiple rounds of analyses by Iterate error
packages, err := c.Catalog(contents)
if err != nil {
errs = multierror.Append(errs, err)
continue
}
log.Debugf("cataloger '%s' discovered '%d' packages", c.Name(), len(packages))
packagesDiscovered.N += int64(len(packages))
catalogerName := c.Name()
for _, p := range packages {
p.FoundBy = catalogerName
catalog.Add(p)
}
}
if errs != nil {
return nil, errs
}
filesProcessed.SetCompleted()
packagesDiscovered.SetCompleted()
return catalog, nil
}
// Monitor provides progress-related data for observing the progress of a Catalog() call (published on the event bus).
type Monitor struct {
FilesProcessed progress.Monitorable // the number of files selected and contents analyzed from all registered catalogers
PackagesDiscovered progress.Monitorable // the number of packages discovered from all registered catalogers
}
// newMonitor creates a new Monitor object and publishes the object on the bus as a CatalogerStarted event.
func newMonitor() (*progress.Manual, *progress.Manual) {
filesProcessed := progress.Manual{}
packagesDiscovered := progress.Manual{}
bus.Publish(partybus.Event{
Type: event.CatalogerStarted,
Value: Monitor{
FilesProcessed: progress.Monitorable(&filesProcessed),
PackagesDiscovered: progress.Monitorable(&packagesDiscovered),
},
})
return &filesProcessed, &packagesDiscovered
}
+50
View File
@@ -0,0 +1,50 @@
/*
Package cataloger provides the ability to process files from a container image or file system and discover packages
(gems, wheels, jars, rpms, debs, etc). Specifically, this package contains both a catalog function to utilize all
catalogers defined in child packages as well as the interface definition to implement a cataloger.
*/
package cataloger
import (
"io"
"github.com/anchore/syft/syft/cataloger/dpkg"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/cataloger/apkdb"
"github.com/anchore/syft/syft/cataloger/bundler"
"github.com/anchore/syft/syft/cataloger/golang"
"github.com/anchore/syft/syft/cataloger/java"
"github.com/anchore/syft/syft/cataloger/javascript"
"github.com/anchore/syft/syft/cataloger/python"
"github.com/anchore/syft/syft/cataloger/rpmdb"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/scope"
)
// Cataloger describes behavior for an object to participate in parsing container image or file system
// contents for the purpose of discovering Packages. Each concrete implementation should focus on discovering Packages
// for a specific Package Type or ecosystem.
type Cataloger interface {
// Name returns a string that uniquely describes a cataloger
Name() string
// SelectFiles discovers and returns specific files that the cataloger would like to inspect the contents of.
SelectFiles(scope.FileResolver) []file.Reference
// Catalog is given the file contents and should return any discovered Packages after analyzing the contents.
Catalog(map[file.Reference]io.Reader) ([]pkg.Package, error)
// TODO: add "IterationNeeded" error to indicate to the driver to continue with another Select/Catalog pass
}
// All returns a slice of all locally defined catalogers (defined in child packages).
func BuiltIn() []Cataloger {
return []Cataloger{
dpkg.New(),
bundler.New(),
python.New(),
rpmdb.New(),
java.New(),
apkdb.New(),
golang.New(),
javascript.New(),
}
}
+103
View File
@@ -0,0 +1,103 @@
/*
Package common provides generic utilities used by multiple catalogers.
*/
package common
import (
"io"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/scope"
)
// GenericCataloger implements the Catalog interface and is responsible for dispatching the proper parser function for
// a given path or glob pattern. This is intended to be reusable across many package cataloger types.
type GenericCataloger struct {
globParsers map[string]ParserFn
pathParsers map[string]ParserFn
selectedFiles []file.Reference
parsers map[file.Reference]ParserFn
}
// NewGenericCataloger if provided path-to-parser-function and glob-to-parser-function lookups creates a GenericCataloger
func NewGenericCataloger(pathParsers map[string]ParserFn, globParsers map[string]ParserFn) GenericCataloger {
return GenericCataloger{
globParsers: globParsers,
pathParsers: pathParsers,
selectedFiles: make([]file.Reference, 0),
parsers: make(map[file.Reference]ParserFn),
}
}
// register pairs a set of file references with a parser function for future cataloging (when the file contents are resolved)
func (a *GenericCataloger) register(files []file.Reference, parser ParserFn) {
a.selectedFiles = append(a.selectedFiles, files...)
for _, f := range files {
a.parsers[f] = parser
}
}
// clear deletes all registered file-reference-to-parser-function pairings from former SelectFiles() and register() calls
func (a *GenericCataloger) clear() {
a.selectedFiles = make([]file.Reference, 0)
a.parsers = make(map[file.Reference]ParserFn)
}
// SelectFiles takes a set of file trees and resolves and file references of interest for future cataloging
func (a *GenericCataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
// select by exact path
for path, parser := range a.pathParsers {
files, err := resolver.FilesByPath(file.Path(path))
if err != nil {
log.Errorf("cataloger failed to select files by path: %w", err)
}
if files != nil {
a.register(files, parser)
}
}
// select by glob pattern
for globPattern, parser := range a.globParsers {
fileMatches, err := resolver.FilesByGlob(globPattern)
if err != nil {
log.Errorf("failed to find files by glob: %s", globPattern)
}
if fileMatches != nil {
a.register(fileMatches, parser)
}
}
return a.selectedFiles
}
// Catalog takes a set of file contents and uses any configured parser functions to resolve and return discovered packages
func (a *GenericCataloger) Catalog(contents map[file.Reference]io.Reader) ([]pkg.Package, error) {
defer a.clear()
packages := make([]pkg.Package, 0)
for reference, parser := range a.parsers {
content, ok := contents[reference]
if !ok {
log.Errorf("cataloger missing file content: %+v", reference)
continue
}
entries, err := parser(string(reference.Path), content)
if err != nil {
// TODO: should we fail? or only log?
log.Errorf("cataloger failed to parse entries (reference=%+v): %w", reference, err)
continue
}
for _, entry := range entries {
entry.Source = []file.Reference{reference}
packages = append(packages, entry)
}
}
return packages, nil
}
@@ -0,0 +1,115 @@
package common
import (
"fmt"
"io"
"io/ioutil"
"testing"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/pkg"
)
type testResolver struct {
contents map[file.Reference]string
}
func newTestResolver() *testResolver {
return &testResolver{
contents: make(map[file.Reference]string),
}
}
func (r *testResolver) FilesByPath(paths ...file.Path) ([]file.Reference, error) {
results := make([]file.Reference, len(paths))
for idx, p := range paths {
results[idx] = file.NewFileReference(p)
r.contents[results[idx]] = fmt.Sprintf("%s file contents!", p)
}
return results, nil
}
func (r *testResolver) FilesByGlob(patterns ...string) ([]file.Reference, error) {
path := "/a-path.txt"
ref := file.NewFileReference(file.Path(path))
r.contents[ref] = fmt.Sprintf("%s file contents!", path)
return []file.Reference{ref}, nil
}
func parser(_ string, reader io.Reader) ([]pkg.Package, error) {
contents, err := ioutil.ReadAll(reader)
if err != nil {
panic(err)
}
return []pkg.Package{
{
Name: string(contents),
},
}, nil
}
func TestGenericCataloger(t *testing.T) {
globParsers := map[string]ParserFn{
"**a-path.txt": parser,
}
pathParsers := map[string]ParserFn{
"/another-path.txt": parser,
"/last/path.txt": parser,
}
resolver := newTestResolver()
cataloger := NewGenericCataloger(pathParsers, globParsers)
selected := cataloger.SelectFiles(resolver)
if len(selected) != 3 {
t.Fatalf("unexpected selection length: %d", len(selected))
}
expectedSelection := internal.NewStringSetFromSlice([]string{"/last/path.txt", "/another-path.txt", "/a-path.txt"})
selectionByPath := make(map[string]file.Reference)
for _, s := range selected {
if !expectedSelection.Contains(string(s.Path)) {
t.Errorf("unexpected selection path: %+v", s.Path)
}
selectionByPath[string(s.Path)] = s
}
expectedPkgs := make(map[file.Reference]pkg.Package)
for path, ref := range selectionByPath {
expectedPkgs[ref] = pkg.Package{
Source: []file.Reference{ref},
Name: fmt.Sprintf("%s file contents!", path),
}
}
actualPkgs, err := cataloger.Catalog(resolver.contents)
if err != nil {
t.Fatalf("cataloger catalog action failed: %+v", err)
}
if len(actualPkgs) != len(expectedPkgs) {
t.Fatalf("unexpected packages len: %d", len(actualPkgs))
}
for _, p := range actualPkgs {
ref := p.Source[0]
exP, ok := expectedPkgs[ref]
if !ok {
t.Errorf("missing expected pkg: ref=%+v", ref)
continue
}
if p.FoundBy != exP.FoundBy {
t.Errorf("bad upstream: %s", p.FoundBy)
}
if exP.Name != p.Name {
t.Errorf("bad contents mapping: %+v", p.Source)
}
}
}
+10
View File
@@ -0,0 +1,10 @@
package common
import (
"io"
"github.com/anchore/syft/syft/pkg"
)
// ParserFn standardizes a function signature for parser functions that accept the virtual file path (not usable for file reads) and contents and return any discovered packages from that file
type ParserFn func(string, io.Reader) ([]pkg.Package, error)
+44
View File
@@ -0,0 +1,44 @@
/*
Package dpkg provides a concrete Cataloger implementation for Debian package DB status files.
*/
package dpkg
import (
"io"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/scope"
)
// Cataloger catalogs pkg.DebPkg Package Types defined in DPKG status files.
type Cataloger struct {
cataloger common.GenericCataloger
}
// New returns a new Deb package cataloger object.
func New() *Cataloger {
globParsers := map[string]common.ParserFn{
"**/var/lib/dpkg/status": parseDpkgStatus,
}
return &Cataloger{
cataloger: common.NewGenericCataloger(nil, globParsers),
}
}
// Name returns a string that uniquely describes this cataloger.
func (c *Cataloger) Name() string {
return "dpkg-cataloger"
}
// SelectFiles returns a set of discovered DPKG status files from the user content source.
func (c *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return c.cataloger.SelectFiles(resolver)
}
// Catalog returns the Packages indexed from all DPKG status files discovered.
func (c *Cataloger) Catalog(contents map[file.Reference]io.Reader) ([]pkg.Package, error) {
return c.cataloger.Catalog(contents)
}
@@ -6,31 +6,42 @@ import (
"io"
"strings"
"github.com/anchore/imgbom/imgbom/pkg"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg"
"github.com/mitchellh/mapstructure"
)
var endOfPackages = fmt.Errorf("no more packages to read")
// integrity check
var _ common.ParserFn = parseDpkgStatus
func ParseEntries(reader io.Reader) ([]pkg.DpkgMetadata, error) {
var errEndOfPackages = fmt.Errorf("no more packages to read")
// parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed.
func parseDpkgStatus(_ string, reader io.Reader) ([]pkg.Package, error) {
buffedReader := bufio.NewReader(reader)
var entries = make([]pkg.DpkgMetadata, 0)
var packages = make([]pkg.Package, 0)
for {
entry, err := parseEntry(buffedReader)
entry, err := parseDpkgStatusEntry(buffedReader)
if err != nil {
if err == endOfPackages {
if err == errEndOfPackages {
break
}
return nil, err
}
entries = append(entries, entry)
packages = append(packages, pkg.Package{
Name: entry.Package,
Version: entry.Version,
Type: pkg.DebPkg,
Metadata: entry,
})
}
return entries, nil
return packages, nil
}
func parseEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err error) {
// parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader.
func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err error) {
dpkgFields := make(map[string]string)
var key string
@@ -38,7 +49,7 @@ func parseEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err error) {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
return pkg.DpkgMetadata{}, endOfPackages
return pkg.DpkgMetadata{}, errEndOfPackages
}
return pkg.DpkgMetadata{}, err
}

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