Compare commits

...
106 Commits
Author SHA1 Message Date
Alex Goodman 773581704c bump threshold for inline compare for jenkins image
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-30 15:14:32 -04:00
Alex Goodman 6e98752c6b Merge pull request #247 from anchore/syft-java-cataloger-integration
General Java cataloger enhancements
2020-10-30 13:55:16 -04:00
Alex Goodman e2593cd6b7 remove extra fields from the compare script metadata namedtuple
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-30 10:59:53 -04:00
Alex Goodman 232cd13035 update tests for enhanced java pkg pairings
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-30 10:44:45 -04:00
Alex Goodman 2532928afa bump java compare testing thresholds
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-30 10:06:47 -04:00
Alex Goodman 03dbfb8dfb improve java name and version extraction as well as parent pkg pairing
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-30 08:12:25 -04:00
Alex Goodman a5cba13ddf enable more flexible java manifest structure (closer to the spec)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-29 16:26:04 -04:00
Alex Goodman a4f22e65fc expand compare testing images
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-29 13:52:51 -04:00
Alex Goodman ab45be98b8 append java nested package names to the virtual path
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-29 13:52:34 -04:00
Alex Goodman fc991bc62e partial java comparison with extra metadata
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-29 12:40:49 -04:00
Alex Goodman 1230650771 allow for java manifest data to be optional
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-29 11:28:27 -04:00
Alex Goodman 452426d1d6 Merge pull request #245 from anchore/update-java-cataloger-for-engine
Java cataloger miscellaneous fixes
2020-10-28 17:36:44 -04:00
Alex Goodman 2675891110 remove parent java package from json && add java manifest section parsing
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-28 17:19:06 -04:00
Dan Luhring bffc4713a7 Update zip archive handling to match globs as expected (#244)
* Refactor zip file tests

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

* Add glob support for leading slashes

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

* Update zip testing to account for glob matching

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

* Ignore .DS_STORE

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

* Improve normalization of zip entry names

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

* Rename zip test helpers file

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-10-28 16:50:40 -04:00
Alex Goodman 62f6146c37 do not include optional fields
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-28 15:58:54 -04:00
Alex Goodman 8bcba7d830 update json schema and test for java virtualPath
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-28 15:25:30 -04:00
Alex Goodman 76c8f24090 add java virtual path
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-28 13:53:44 -04:00
Alex Goodman cc466e47da bump python version for acceptance tests
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-27 14:53:52 -04:00
Alex Goodman f9407d0ce4 Add java/npm to inline comparison (#235)
* Adds java and npm package comparison
* Adds probable matching of extra packages syft found and missing packages that syft did not find (but inline did). This way there is a section of output that fuzzy-matches the package names to get a better sense of "real" problems (actual missing packages) vs slightly mismatched metadata during troubleshooting.
* Adds a set or probable missing packages to the report based on the probable matches (again, to aid in troubleshooting)
* Fixes image reference clean function to support references with registries
* Only shows metadata differences when the package was found by both inline and syft
* Splits the inline-compare code into more manageable pieces

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-27 13:43:36 -04:00
Dan Luhring f19cb03aa0 Update doublestar to include fix for open dirs issue (#240)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-10-26 11:19:25 -04:00
Toure Dunnon 076454d7a8 Merge pull request #233 from anchore/javascript_parser_fix_author
Javascript parser fix author
2020-10-26 09:30:11 -04:00
Toure Dunnon 7c42a7441c Merge branch 'javascript_parser_fix_author' of https://github.com/anchore/syft into javascript_parser_fix_author 2020-10-23 16:49:51 -04:00
Toure Dunnon cbb7e00a52 Cleaned up go.mod import.
Signed-off-by: Toure Dunnon <toure.dunnon@anchore.com>
2020-10-23 16:44:07 -04:00
Alex Goodman 6eb36bc3c8 allow for optional fields in npm metadata
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-23 15:00:55 -04:00
Toure Dunnon 94ffc2caa8 Merge branch 'main' into javascript_parser_fix_author
Signed-off-by: Toure Dunnon <toure.dunnon@anchore.com>
2020-10-23 12:05:30 -04:00
Alex Goodman 15e2e32103 Merge pull request #237 from anchore/update-future-plans
Remove CycloneDX from future plans
2020-10-23 11:06:51 -04:00
Alex Goodman 1cdf2b9151 remove cyclonedx from future plans (already implemented)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-23 11:05:28 -04:00
Alex Goodman fd65f363ce Merge pull request #236 from anchore/fix-acceptance-test-pipeline
Bootstrap cached dependencies and CI dependencies separately for acceptance tests
2020-10-23 10:55:24 -04:00
Alex Goodman 103f0617f5 bootstrap cached deps and ci deps separately for acceptance tests
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-23 10:42:57 -04:00
Alex Goodman de2e6a13b8 Merge pull request #234 from anchore/split-python-cataloger-with-cataloger-addition
Split python cataloger by image/directory scanning + add more metadata
2020-10-23 10:37:01 -04:00
Alex Goodman 7d55bca0a0 allow for python metadata fields to be optional
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-23 10:29:52 -04:00
Toure 48c7dee9da included additional support for older npm spec.
Signed-off-by: Toure <tdunnon@gmail.com>
2020-10-23 08:50:38 -04:00
Toure 94e448a818 Merge commit '15379d1' into javascript_parser_fix_author
Signed-off-by: Toure <tdunnon@gmail.com>
2020-10-22 14:08:33 -04:00
Toure Dunnon 15379d1075 Fix for errors+failures parsing package.json
closes: #230

Signed-off-by: Toure Dunnon <toure.dunnon@anchore.com>
2020-10-22 13:58:18 -04:00
Alex Goodman 2e5ff4a995 add top_level.txt processing to python package cataloger
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-22 12:39:03 -04:00
Alex Goodman 1414d1fbc3 add test coverage for python pacakge cataloger and update catalog interface
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-22 09:51:29 -04:00
Alex Goodman 7fc926d40d add python author and emil fields + add metadata type
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-22 09:51:29 -04:00
Toure Dunnon 5b08616e47 Fix for errors+failures parsing package.json
closes: #230

Signed-off-by: Toure Dunnon <toure.dunnon@anchore.com>
2020-10-21 14:11:35 -04:00
Alex Goodman 0ce8701e73 split python package catalogers by image vs directory
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-21 11:48:26 -04:00
Alex Goodman beb6afff36 Merge pull request #232 from anchore/cataloger-interface-upgrade
Upgrade cataloger interface to use full resolver
2020-10-21 11:47:52 -04:00
Alex Goodman d4ca0ab167 expand the resolver to include content requests for a single reference
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-21 11:40:14 -04:00
Alex Goodman 6cd9c2b771 upgrade cataloger interface to use full resolver (remove SelectFiles function)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-21 10:49:33 -04:00
Alex Goodman 931c796158 add dynamic package.json parsing of author field
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-20 15:23:04 -04:00
Alex Goodman bb14f3b45b reduce number of open files while processing nested java archives (#227)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-16 17:22:14 -04:00
Toure Dunnon c0b9d7854b Merge pull request #224 from anchore/add_package_json
Add support for package.json #200
2020-10-16 11:43:27 -04:00
Toure Dunnon 27c62e34f2 Add support for package.json #200
Signed-off-by: Toure Dunnon <toure.dunnon@anchore.com>
2020-10-16 11:28:54 -04:00
Alex Goodman 4c751cb1d4 Merge pull request #222 from VinodAnandan/main
Enable CodeQL Security Scan
2020-10-15 23:11:01 -04:00
Vinod Anandan 0f92f16eb6 Enable CodeQL Security Scan
https://github.blog/2020-09-30-code-scanning-is-now-available/

remove java from codeql scan

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
Signed-off-by: Vinod Anandan <vinod.anandan@jpmorgan.com>
2020-10-16 03:02:09 +01:00
Alex Goodman 1bf1e643eb restore original release token
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-15 14:41:28 -04:00
Alex Goodman 3ba67a6bcc update json schema to account for optional gemspec fields (#225)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-15 13:56:59 -04:00
Alex Goodman 4a8ee15e87 fix syft version verbiage
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-15 11:50:23 -04:00
Alex Goodman dcb5d6d08e remove greeter action since it is broken for forked PRs (#223)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-15 10:20:09 -04:00
Alex Goodman b3387807c1 allow for gemspec metadata fields to be optional (#218)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-15 09:57:58 -04:00
Alex Goodman 24c4c025e1 Merge pull request #217 from jhujasonw/main
Fixes #212 by increasing buffer size for scanner
2020-10-14 07:29:00 -04:00
Alex Goodman 5d0909f31e add regression test to cover issue #212
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-14 07:22:19 -04:00
Jason Williams 1f64b3cd49 Fixes #212 by increasing buffer size for scanner
Signed-off-by: Jason Williams <jasonw@jhu.edu>
2020-10-13 11:00:55 -04:00
Toure Dunnon 8d25d44ea9 Add homepage field as output to the gemspec metadata (#215)
https://github.com/anchore/syft/issues/214

Signed-off-by: Toure <tdunnon@gmail.com>
2020-10-12 18:59:56 -04:00
Alex Goodman 1fc46291a6 update install script arguments for proper argument processing (#211)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-09 11:04:53 -04:00
Alex Goodman 2b932f8d65 run tests on fork PRs (#210)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-09 07:45:05 -04:00
Alex Goodman 0d3e153959 replace circle ci badge with GHA (#209)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-08 15:45:26 -04:00
587589bfe3 Add first issue/PR welcome message action (#185)
* Add first issue/PR welcome message action

Signed-off-by: Robert Prince <robert.prince@anchore.com>

* update first-pr-issue message with a simple greeting

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

Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
2020-10-08 15:42:46 -04:00
Alfredo Deza 4ff330e396 Merge pull request #208 from anchore/non-prerelase
docs: update the readme to remove pre-release note
2020-10-08 15:34:06 -04:00
Alfredo Deza 949bf7bc91 docs: update the readme to remove pre-release note
Signed-off-by: Alfredo Deza <adeza@anchore.com>
2020-10-08 15:30:50 -04:00
Alex Goodman 956af57d3b update release token name
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-08 14:37:06 -04:00
Alex Goodman 262a0888a8 fix release pipeline to wait for GHA check names
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-08 13:57:43 -04:00
Alex Goodman 335a6b022f add acceptance test to cover gem cataloger + fix gem cataloger parent dir (#207)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-08 13:13:31 -04:00
Alex Goodman da0eb6f20f fix acceptance tests & add notification upon failures (#204)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-08 11:07:00 -04:00
Toure Dunnon 1be5f5756a Merge pull request #203 from anchore/syft_bundler_wip
Add gemspec support
2020-10-08 11:03:30 -04:00
Alex Goodman 9edbc65bce move unicode regex to static space
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-08 10:55:57 -04:00
Alex Goodman 398d8903e7 finalize gemspec parser + update json schema to match
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-08 10:52:14 -04:00
Alex Goodman 46c74865e5 add render unicode in gemspec parser
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-08 10:52:14 -04:00
Alex Goodman 1f0f6fa3e5 more gemspec tests
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-08 10:52:03 -04:00
Alex Goodman abdd00cd24 rename gem parsers and catalogers
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-08 10:23:24 -04:00
Alex Goodman 10b44f5311 split catalogers into two sets, one for images another for directory scans
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-08 10:23:24 -04:00
Alex Goodman 1c320a8382 defer to GenericCataloger instances for pkg catalogers
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-08 10:23:24 -04:00
Toure 7a8a5419b8 adding ruby gemspec support.
Signed-off-by: Toure Dunnon <toure.dunnon@anchore.com>
2020-10-08 10:19:33 -04:00
Alex Goodman 16b23e7994 add gha pipeline to replace circlei pipeline (#202)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-07 12:20:51 -04:00
Alex Goodman e6b71486b0 modify version command to match grype (#196)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-10-06 13:13:52 -04:00
Dan Luhring 5adfce19e0 Set prerelease to auto (#189)
Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-09-29 16:33:40 -04:00
Dan Luhring b6dfdf16b6 Remove and update TODOs (#194)
* Remove and update TODOs

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

* Update TODO with link

Signed-off-by: Dan Luhring <dan.luhring@anchore.com>
2020-09-29 11:53:19 -04:00
Alex Goodman b5ce187ba7 replace modfile with stdlib (#187)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-28 17:25:04 -04:00
Alex Goodman 26855a2a9e ignore apk xattr file checksum + remove log.Errorf error wraps (#192)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-28 17:22:17 -04:00
Alex Goodman 4b78d9a1c0 support home dir expansion (#188)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-28 15:02:21 -04:00
Alex Goodman 2844b9878f add release notification
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-25 23:21:16 -04:00
Alex Goodman 8b81c87d18 remove unreleased tags and exclude size labels
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-25 22:59:19 -04:00
Alex Goodman 9bd9dad76c remove unrelease changelog option
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-25 18:11:02 -04:00
Alex Goodman 6d9f9a9b3b pin the two tags used for release autochangelog
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-25 17:57:51 -04:00
Alex Goodman 50cb5612d0 fix github releaser changelog intake
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-25 17:20:27 -04:00
Alex Goodman 080f010746 remove tty requirement from release process
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-25 17:07:59 -04:00
Alex Goodman f4502fc824 Add notifications around the release process (#184)
* add pipeline notification upon release

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

* add pending notification to release pipeline

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-25 16:58:56 -04:00
Alex Goodman 03378e976c always return a cleanup function from scope (#183)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-25 16:20:06 -04:00
Alex Goodman 576fbc898b rm stereoscope from testutils (#181)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-25 14:53:05 -04:00
Alex Goodman 49800b6747 Normalize the json image/dir source (#180)
* normalize the json image/dir source

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

* update json image presenter golden file

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-25 14:07:38 -04:00
Alex Goodman c46d004a3b remove duplicate rows from the summary table (#179)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-25 10:32:37 -04:00
Alex Goodman 3d91a66536 Add OCI support + use URI schemes for user image input (#178)
* add oci support + update image schemes

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

* update docs to reflect OCI image sources + URI scheme change

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

* update to oci-dir

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

* bump upstream stereoscope pin

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-25 10:24:24 -04:00
Alex Goodman 45b5cab49a unpin skopeo during mac acceptance tests (#176)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2020-09-18 13:54:40 -04:00
Sam Dacanay 1866949eff Merge pull request #175 from anchore/fix-json-keys
Change kebab case to camelCase in json keys throughout app
2020-09-18 09:19:37 -07:00
Samuel Dacanay 0567206b38 Change kebab case to camelCase in json keys throughout app
Signed-off-by: Samuel Dacanay <sam.dacanay@anchore.com>
2020-09-18 08:55:17 -07:00
Alex GoodmanandAlfredo Deza f3076c3905 Add docs around the main execution path (#172)
* add docs around the main execution path of syft

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

* Update syft/lib.go

Co-authored-by: Alfredo Deza <adeza@anchore.com>
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* Update syft/lib.go

Co-authored-by: Alfredo Deza <adeza@anchore.com>
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* wrap line

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

Co-authored-by: Alfredo Deza <adeza@anchore.com>
2020-09-17 06:02:51 -04:00
Samuel Dacanay b467964044 port over shell completion w/ cobra from grype, find/replace, etc.
Signed-off-by: Samuel Dacanay <sam.dacanay@anchore.com>
2020-09-15 09:07:17 -07:00
Praveen Mylavarapu e26627d8b6 remove accessing tag when not available (#174)
Signed-off-by: praveenmylavarapu <saipraveenmylavarapu@gmail.com>
2020-09-15 07:55:08 -04:00
Sam Dacanay 80ee18d20b Merge pull request #171 from anchore/version-output-json
Add support for JSON version output
2020-09-14 13:51:59 -07:00
Samuel Dacanay ef64a6f206 Add support for JSON version output
Signed-off-by: Samuel Dacanay <sam.dacanay@anchore.com>

Make json version output always verbose, cleanup struct tag

Signed-off-by: Samuel Dacanay <sam.dacanay@anchore.com>

Use camel case for json

Signed-off-by: Samuel Dacanay <sam.dacanay@anchore.com>
2020-09-14 13:48:27 -07:00
Sam Dacanay 22dfcb2d0b Merge pull request #170 from anchore/add-distro-mappings-arch-and-opensuseleap
Add support for Arch (which doesnt return a version) and Open Suse Leap
2020-09-14 09:02:28 -07:00
Samuel Dacanay 329b080a37 Add support for Arch (which doesnt return a version) and Open Suse Leap
Signed-off-by: Samuel Dacanay <sam.dacanay@anchore.com>

remove bad-version test fixture (no longer relevant since it is now an expected case) and add cases for arch and opensuse-leap

Signed-off-by: Samuel Dacanay <sam.dacanay@anchore.com>

Reduce nesting, simplify empty version logic

Signed-off-by: Samuel Dacanay <sam.dacanay@anchore.com>
2020-09-13 13:50:56 -07:00
Sam Dacanay 11334b632e Expand matching of requirements.txt file to include any prefixes or suffixes (#168)
* Expand matching of requirements.txt file to include any prefixes or suffixes

Signed-off-by: Samuel Dacanay <sam.dacanay@anchore.com>

* Add some test cases to integration test (ensure syft can pick up multiple requirements files)

Signed-off-by: Samuel Dacanay <sam.dacanay@anchore.com>

* Run lint-fix

Signed-off-by: Samuel Dacanay <sam.dacanay@anchore.com>
2020-09-08 10:23:37 -07:00
174 changed files with 5406 additions and 2407 deletions
-129
View File
@@ -1,129 +0,0 @@
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"
+1 -1
View File
@@ -16,5 +16,5 @@ assignees: ''
**Anything else we need to know?**:
**Environment**:
- syft version (use `syft version -v`):
- Output of `syft version`:
- OS (e.g: `cat /etc/os-release` or similar):
+48 -6
View File
@@ -14,7 +14,8 @@ env:
jobs:
Build-Snapshot-Artifacts:
runs-on: ubuntu-latest
# come Nov 30 2020 ubuntu-latest will be ubuntu-20.04, until then it needs to be explicitly referenced due to python 3.7 specific features being used
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-go@v2
@@ -35,8 +36,11 @@ jobs:
${{ 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'
- name: Bootstrap project dependencies
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
run: make bootstrap
- name: Bootstrap CI dependencies
run: make ci-bootstrap
- name: Import GPG key
@@ -64,10 +68,20 @@ jobs:
name: artifacts
path: snapshot/**/*
- uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,workflow,job,commit,message,author
text: The syft acceptance tests have failed tragically!
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ failure() }}
# 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
# come Nov 30 2020 ubuntu-latest will be ubuntu-20.04, until then it needs to be explicitly referenced due to python 3.7 specific features being used
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
@@ -80,6 +94,15 @@ jobs:
- name: Run Acceptance Tests (Linux)
run: make acceptance-linux
- uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,workflow,job,commit,message,author
text: The syft acceptance tests have failed tragically!
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ failure() }}
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Acceptance-Mac:
needs: [ Build-Snapshot-Artifacts ]
@@ -96,10 +119,20 @@ jobs:
- name: Run Acceptance Tests (Mac)
run: make acceptance-mac
- uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,workflow,job,commit,message,author
text: The syft acceptance tests have failed tragically!
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ failure() }}
# 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
# come Nov 30 2020 ubuntu-latest will be ubuntu-20.04, until then it needs to be explicitly referenced due to python 3.7 specific features being used
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
@@ -120,4 +153,13 @@ jobs:
path: snapshot
- name: Compare Anchore inline-scan results against snapshot build output
run: make compare-snapshot
run: make compare-snapshot
- uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,workflow,job,commit,message,author
text: The syft acceptance tests have failed tragically!
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ failure() }}
+71
View File
@@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL Security Scan"
on:
push:
branches: [main]
pull_request:
# The branches below must be a subset of the branches above
branches: [main]
schedule:
- cron: '0 0 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['go', 'python']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# ℹ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+24 -5
View File
@@ -25,13 +25,22 @@ jobs:
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
- name: Check static analysis results
uses: fountainhead/action-wait-for-check@v1.0.0
id: sa-unit-int
id: static-analysis
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"
checkName: "Static-Analysis (1.x, ubuntu-latest)"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check unit + integration results (latest go version)
uses: fountainhead/action-wait-for-check@v1.0.0
id: unit-integration
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the circle-ci workflow name (in .circleci/config.yaml)
checkName: "Tests (1.x, ubuntu-latest)"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (linux)
@@ -62,9 +71,10 @@ jobs:
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'
if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit-integration.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 "Static Analysis Status: ${{ steps.static-analysis.conclusion }}"
echo "Unit & Integration Test Status: ${{ steps.unit-integration.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 }}"
@@ -123,6 +133,15 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.TOOLBOX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.TOOLBOX_AWS_SECRET_ACCESS_KEY }}
- uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,workflow,action,eventName
text: "A new Syft release is ready to be manually published: https://github.com/anchore/syft/releases"
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ success() }}
- uses: actions/upload-artifact@v2
with:
name: artifacts
@@ -0,0 +1,104 @@
name: 'Static Analysis + Unit + Integration'
on:
workflow_dispatch:
push:
pull_request:
jobs:
Static-Analysis:
strategy:
matrix:
go-version: [1.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- uses: actions/checkout@v2
- name: Restore bootstrap cache
id: bootstrap-cache
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
${{ github.workspace }}/.tmp
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
restore-keys: |
${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-
${{ runner.os }}-go-${{ matrix.go-version }}-
- name: Bootstrap project dependencies
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
run: make bootstrap
- name: Bootstrap CI dependencies
run: make ci-bootstrap
- name: Run static analysis
run: make static-analysis
Tests:
strategy:
matrix:
# test the lower bounds of support, and the latest available
go-version: [1.13.x, 1.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- uses: actions/checkout@v2
- name: Restore bootstrap cache
id: bootstrap-cache
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
${{ github.workspace }}/.tmp
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
restore-keys: |
${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-
${{ runner.os }}-go-${{ matrix.go-version }}-
- name: Bootstrap project dependencies
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
run: make bootstrap
- name: Bootstrap CI dependencies
run: make ci-bootstrap
- name: Build cache key for java test-fixture blobs (for unit tests)
run: make java-packages-fingerprint
- name: Restore Java test-fixture cache
id: unit-java-cache
uses: actions/cache@v2
with:
path: syft/cataloger/java/test-fixtures/java-builds/packages
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/cataloger/java/test-fixtures/java-builds/packages.fingerprint' ) }}
- name: Run unit tests
run: make unit
- name: Validate syft output against the CycloneDX schema
run: make validate-cyclonedx-schema
- name: Build key for tar cache
run: make integration-fingerprint
- name: Restore integration test cache
uses: actions/cache@v2
with:
path: ${{ github.workspace }}/integration/test-fixtures/cache
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('integration/test-fixtures/cache.fingerprint') }}
- name: Run integration tests
run: make integration
+3
View File
@@ -29,3 +29,6 @@ coverage.txt
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# macOS Finder metadata
.DS_STORE
+1 -1
View File
@@ -1,7 +1,7 @@
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
prerelease: auto
# If set to true, will not auto-publish the release. This is done to allow us to review the changelog before publishing.
draft: true
+43 -26
View File
@@ -27,6 +27,10 @@ ifeq "$(strip $(VERSION))" ""
override VERSION = $(shell git describe --always --tags --dirty)
endif
# used to generate the changelog from the second to last tag to the current tag (used in the release pipeline when the release tag is in place)
LAST_TAG := $(shell git describe --abbrev=0 --tags $(shell git rev-list --tags --max-count=1))
SECOND_TO_LAST_TAG := $(shell git describe --abbrev=0 --tags $(shell git rev-list --tags --skip=1 --max-count=1))
## Variable assertions
ifndef TEMPDIR
@@ -67,7 +71,7 @@ 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
ci-bootstrap:
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y bc jq libxml2-utils
.PHONY: bootstrap
@@ -118,7 +122,7 @@ validate-cyclonedx-schema:
.PHONY: unit
unit: fixtures ## Run unit tests (with coverage)
$(call title,Running unit tests)
go test -coverprofile $(COVER_REPORT) ./...
go test -coverprofile $(COVER_REPORT) $(shell go list ./... | grep -v anchore/syft/test)
@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
@@ -126,11 +130,13 @@ unit: fixtures ## Run unit tests (with coverage)
.PHONY: integration
integration: ## Run integration tests
$(call title,Running integration tests)
go test -v -tags=integration ./test/integration
go test -v ./test/integration
# 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
find test/integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/integration/test-fixtures/cache.fingerprint
.PHONY: java-packages-fingerprint
java-packages-fingerprint:
@@ -157,17 +163,7 @@ generate-json-schema: clean-json-schema-examples integration ## Generate a new j
.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'
find . -type f -wholename "**/test-fixtures/cache/*.tar" -delete
.PHONY: build
build: $(SNAPSHOTDIR) ## Build release snapshot binaries and packages
@@ -226,32 +222,53 @@ acceptance-test-rpm-package-install: $(SNAPSHOTDIR)
$(ACC_TEST_IMAGE) \
$(RESULTSDIR)
.PHONY: changlog
changelog:
mkdir -p $(DISTDIR)
.PHONY: changlog-release
changelog-release:
@echo "Last tag: $(SECOND_TO_LAST_TAG)"
@echo "Current tag: $(VERSION)"
@docker run -i --rm \
-v "$(shell pwd)":/usr/local/src/your-app ferrarimarco/github-changelog-generator \
--user anchore \
--project $(BIN) \
-t ${GITHUB_TOKEN} \
--exclude-labels 'duplicate,question,invalid,wontfix,size:small,size:medium,size:large,size:x-large' \
--no-pr-wo-labels \
--no-issues-wo-labels \
--since-tag $(SECOND_TO_LAST_TAG)
@printf '\n$(BOLD)$(CYAN)Release $(VERSION) Changelog$(RESET)\n\n'
@cat CHANGELOG.md
.PHONY: changelog-unreleased
changelog-unreleased: ## show the current changelog that will be produced on the next release (note: requires GITHUB_TOKEN set)
@docker run -it --rm \
-v "$(shell pwd)":/usr/local/src/your-app ferrarimarco/github-changelog-generator \
--user anchore \
--project $(BIN) \
-t ${GITHUB_TOKEN} \
--no-pr-wo-labels \
--no-issues-wo-labels \
--unreleased-only \
--future-release $(VERSION)
--exclude-labels 'duplicate,question,invalid,wontfix,size:small,size:medium,size:large,size:x-large' \
--since-tag $(LAST_TAG)
@printf '\n$(BOLD)$(CYAN)Unreleased Changes (closed PRs and issues will not be in the final changelog)$(RESET)\n'
@docker run -it --rm \
-v $(shell pwd)/CHANGELOG.md:/CHANGELOG.md \
rawkode/mdv \
-t 748.5989 \
/CHANGELOG.md
.PHONY: release
release: clean-dist changelog ## Build and publish final binaries and packages
release: clean-dist changelog-release ## 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 \
bash -c "BUILD_GIT_TREE_STATE=$(GITTREESTATE) $(TEMPDIR)/goreleaser \
--rm-dist \
--config $(TEMPDIR)/goreleaser.yaml \
--release-notes <(cat CHANGELOG.md)
--release-notes <(cat CHANGELOG.md)"
# verify checksum signatures
.github/scripts/verify-signature.sh "$(DISTDIR)"
+17 -11
View File
@@ -1,6 +1,7 @@
# syft
[![CircleCI](https://circleci.com/gh/anchore/syft.svg?style=svg)](https://circleci.com/gh/anchore/syft)
[![Static Analysis + Unit + Integration](https://github.com/anchore/syft/workflows/Static%20Analysis%20+%20Unit%20+%20Integration/badge.svg)](https://github.com/anchore/syft/actions?query=workflow%3A%22Static+Analysis+%2B+Unit+%2B+Integration%22)
[![Acceptance](https://github.com/anchore/syft/workflows/Acceptance/badge.svg)](https://github.com/anchore/syft/actions?query=workflow%3AAcceptance)
[![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)
@@ -13,12 +14,13 @@ A CLI tool and go library for generating a Software Bill of Materials (SBOM) fro
- 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)
- Supports Docker and OCI image formats
> :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).
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:
To generate an SBOM for a Docker or OCI image:
```
syft <image>
```
@@ -32,19 +34,24 @@ 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 container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
syft path/to/image.tar
# catalog a directory
syft dir://path/to/dir
syft path/to/dir
```
By default Syft shows a summary table, however, more detailed `text` and `json` formats are also available.
The output format for Syft is configurable as well:
```
syft <image> -o json
syft <image> -o text
syft <image> -o <format>
```
Where the `format`s available are:
- `json`: Use this to get as much information out of Syft as possible!
- `text`: A row-oriented, human-and-machine-friendly output.
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.2](https://cyclonedx.org/) specification.
- `table`: A columnar summary (default).
## Installation
**Recommended**
@@ -53,7 +60,7 @@ syft <image> -o text
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>
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b <SOME_BIN_PATH> <RELEASE_VERSION>
```
**macOS**
@@ -105,5 +112,4 @@ 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
+65
View File
@@ -0,0 +1,65 @@
package cmd
import (
"os"
"github.com/spf13/cobra"
)
// completionCmd represents the completion command
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish]",
Short: "Generate a shell completion for Syft (listing local docker images)",
Long: `To load completions (docker image list):
Bash:
$ source <(syft completion bash)
# To load completions for each session, execute once:
Linux:
$ syft completion bash > /etc/bash_completion.d/syft
MacOS:
$ syft completion bash > /usr/local/etc/bash_completion.d/syft
Zsh:
# If shell completion is not already enabled in your environment you will need
# to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ syft completion zsh > "${fpath[1]}/_syft"
# You will need to start a new shell for this setup to take effect.
Fish:
$ syft completion fish | source
# To load completions for each session, execute once:
$ syft completion fish > ~/.config/fish/completions/syft.fish
`,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish"},
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
var err error
switch args[0] {
case "bash":
err = cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
err = cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
err = cmd.Root().GenFishCompletion(os.Stdout, true)
}
if err != nil {
panic(err)
}
},
}
func init() {
rootCmd.AddCommand(completionCmd)
}
+55 -4
View File
@@ -1,8 +1,10 @@
package cmd
import (
"context"
"fmt"
"os"
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
@@ -12,6 +14,9 @@ import (
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/presenter"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
"github.com/wagoodman/go-partybus"
)
@@ -21,10 +26,15 @@ var rootCmd = &cobra.Command{
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
{{.appName}} yourrepo/yourimage:tag defaults to using images from a Docker daemon
{{.appName}} path/to/yourproject a Docker tar, OCI tar, OCI directory, or generic filesystem directory
You can also explicitly specify the scheme to use:
{{.appName}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
{{.appName}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
{{.appName}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Podman or otherwise)
{{.appName}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
{{.appName}} dir:path/to/yourproject read directly from a path on disk (any directory)
`, map[string]interface{}{
"appName": internal.ApplicationName,
}),
@@ -44,6 +54,20 @@ Supports the following image sources:
os.Exit(1)
}
},
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
// Since we use ValidArgsFunction, Cobra will call this AFTER having parsed all flags and arguments provided
dockerImageRepoTags, err := ListLocalDockerImages(toComplete)
if err != nil {
// Indicates that an error occurred and completions should be ignored
return []string{"completion failed"}, cobra.ShellCompDirectiveError
}
if len(dockerImageRepoTags) == 0 {
return []string{"no docker images found"}, cobra.ShellCompDirectiveError
}
// ShellCompDirectiveDefault indicates that the shell will perform its default behavior after completions have
// been provided (without implying other possible directives)
return dockerImageRepoTags, cobra.ShellCompDirectiveDefault
},
}
func startWorker(userInput string) <-chan error {
@@ -88,3 +112,30 @@ func doRunCmd(_ *cobra.Command, args []string) error {
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
return ux(errs, eventSubscription)
}
func ListLocalDockerImages(prefix string) ([]string, error) {
var repoTags = make([]string, 0)
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return repoTags, err
}
// Only want to return tagged images
imageListArgs := filters.NewArgs()
imageListArgs.Add("dangling", "false")
images, err := cli.ImageList(ctx, types.ImageListOptions{All: false, Filters: imageListArgs})
if err != nil {
return repoTags, err
}
for _, image := range images {
// image may have multiple tags
for _, tag := range image.RepoTags {
if strings.HasPrefix(tag, prefix) {
repoTags = append(repoTags, tag)
}
}
}
return repoTags, nil
}
+28 -6
View File
@@ -1,14 +1,18 @@
package cmd
import (
"encoding/json"
"fmt"
"os"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/presenter"
"github.com/spf13/cobra"
)
var showVerboseVersionInfo bool
var outputFormat string
var versionCmd = &cobra.Command{
Use: "version",
@@ -17,14 +21,15 @@ var versionCmd = &cobra.Command{
}
func init() {
versionCmd.Flags().BoolVarP(&showVerboseVersionInfo, "verbose", "v", false, "show additional version information")
versionCmd.Flags().StringVarP(&outputFormat, "output", "o", string(presenter.TextPresenter), "format to show version information (available=[text, json])")
rootCmd.AddCommand(versionCmd)
}
func printVersion(_ *cobra.Command, _ []string) {
versionInfo := version.FromBuild()
if showVerboseVersionInfo {
switch outputFormat {
case "text":
fmt.Println("Application: ", internal.ApplicationName)
fmt.Println("Version: ", versionInfo.Version)
fmt.Println("BuildDate: ", versionInfo.BuildDate)
@@ -33,7 +38,24 @@ func printVersion(_ *cobra.Command, _ []string) {
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)
case "json":
enc := json.NewEncoder(os.Stdout)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
err := enc.Encode(&struct {
version.Version
Application string `json:"application"`
}{
Version: versionInfo,
Application: internal.ApplicationName,
})
if err != nil {
fmt.Printf("failed to show version information: %+v\n", err)
os.Exit(1)
}
default:
fmt.Printf("unsupported output format: %s\n", outputFormat)
os.Exit(1)
}
}
+7 -13
View File
@@ -5,17 +5,15 @@ go 1.14
require (
github.com/adrg/xdg v0.2.1
github.com/anchore/go-rpmdb v0.0.0-20200811175839-cbc751c28e8e
github.com/anchore/go-testutils v0.0.0-20200624184116-66aa578126db
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/stereoscope v0.0.0-20200813152757-548b22c8a0b3
github.com/bmatcuk/doublestar v1.3.1
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe // indirect
github.com/anchore/stereoscope v0.0.0-20200925184903-c82da54e98fe
github.com/bmatcuk/doublestar v1.3.3
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/go-test/deep v1.0.6
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-multierror v1.1.0
github.com/hashicorp/go-version v1.2.0
github.com/mitchellh/go-homedir v1.1.0
@@ -23,10 +21,10 @@ require (
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/afero v1.2.2
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905
github.com/spf13/viper v1.7.0
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
@@ -34,10 +32,6 @@ require (
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
gopkg.in/ini.v1 v1.57.0 // indirect
golang.org/x/mod v0.3.0
gopkg.in/yaml.v2 v2.3.0
)
+26 -37
View File
@@ -104,10 +104,10 @@ github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvo
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA=
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
github.com/Microsoft/hcsshim v0.8.9 h1:VrfodqvztU8YSOvygU+DN1BGaSGxmrNfqOv5oOuX2Bk=
github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab h1:9pygWVFqbY9lPxM0peffumuVDyMuIMzNLyO9uFjJuQo=
github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
github.com/Microsoft/hcsshim v0.8.10 h1:k5wTrpnVU2/xv8ZuzGkbXVd3js5zJ8RnumPo5RxiIxU=
github.com/Microsoft/hcsshim v0.8.10/go.mod h1:g5uw8EV2mAlzqe94tfNBNdr89fnbD/n3HV0OhsddkmM=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us=
@@ -126,13 +126,12 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/anchore/go-rpmdb v0.0.0-20200811175839-cbc751c28e8e h1:kty6r0R2JeaNPeWKSYDC+HW3hkqwFh4PP5TQ8pUPYFw=
github.com/anchore/go-rpmdb v0.0.0-20200811175839-cbc751c28e8e/go.mod h1:iYuIG0Nai8dR0ri3LhZQKUyO1loxUWAGvoWhXDmjy1A=
github.com/anchore/go-testutils v0.0.0-20200624184116-66aa578126db h1:LWKezJnFTFxNkZ4MzajVf+YWvJS0+7hwFr59u6SS7cw=
github.com/anchore/go-testutils v0.0.0-20200624184116-66aa578126db/go.mod h1:D3rc2L/q4Hcp9eeX6AIJH4Q+kPjOtJCFhG9za90j+nU=
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ=
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods=
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/stereoscope v0.0.0-20200520221116-025e07f1c93e/go.mod h1:bkyLl5VITnrmgErv4S1vDfVz/TGAZ5il6161IQo7w2g=
github.com/anchore/stereoscope v0.0.0-20200813152757-548b22c8a0b3 h1:pl+txuYlhK8Mmio4d+4zQI/1xg8X6BtNErTASrx23Wk=
github.com/anchore/stereoscope v0.0.0-20200813152757-548b22c8a0b3/go.mod h1:WntReQTI/I27FOQ87UgLVVzWgku6+ZsqfOTLxpIZFCs=
github.com/anchore/stereoscope v0.0.0-20200925184903-c82da54e98fe h1:m4NSyTo2fVUoUHAV/ZVqE/PFMr/y8oz9HRrhWLk9It0=
github.com/anchore/stereoscope v0.0.0-20200925184903-c82da54e98fe/go.mod h1:2Jja/4l0zYggW52og+nn0rut4i+OYjCf9vTyrM8RT4E=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ=
github.com/apex/log v1.3.0 h1:1fyfbPvUwD10nMoh3hY6MXzvZShJQn9/ck7ATgAt5pA=
@@ -163,6 +162,8 @@ github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAw
github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bmatcuk/doublestar v1.3.1 h1:rT8rxDPsavp9G+4ZULzqhhUSaI/OPsTZNG88Z3i0xvY=
github.com/bmatcuk/doublestar v1.3.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/bmatcuk/doublestar v1.3.3 h1:pVP1d49CcQQaNOl+PI6sPybIrIOD/6sux31PFdmhTH0=
github.com/bmatcuk/doublestar v1.3.3/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/bombsimon/wsl/v2 v2.0.0/go.mod h1:mf25kr/SqFEPhhcxW1+7pxzGlW+hIl/hYTKY95VwV8U=
github.com/bombsimon/wsl/v2 v2.2.0/go.mod h1:Azh8c3XGEJl9LyX0/sFC+CKMc7Ssgua0g+6abzXN4Pg=
github.com/bombsimon/wsl/v3 v3.0.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
@@ -181,10 +182,11 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8=
github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko=
github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM=
github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw=
github.com/containerd/containerd v1.3.0 h1:xjvXQWABwS2uiv3TWgQt5Uth60Gu86LTGZXMJkjc7rY=
github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
@@ -193,7 +195,6 @@ github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMX
github.com/containerd/containerd v1.3.4 h1:3o0smo5SKY7H6AJCmJhsnCjR2/V2T8VmiHt7seN2/kI=
github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY=
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe h1:PEmIrUvwG9Yyv+0WKZqjXfSFDeZjs/q15g0m08BYS9k=
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI=
@@ -209,10 +210,12 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -321,7 +324,7 @@ github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b h1:khEcpUM4yFcxg4
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b h1:ekuhfTjngPhisSjOJ0QWKpPQE8/rbknHaes6WVJj5Hw=
github.com/gofrs/flock v0.0.0-20190320160742-5135e617513b/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -399,11 +402,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-containerregistry v0.0.0-20200430153450-5cbd060f5c92/go.mod h1:pD1UFYs7MCAx+ZLShBdttcaOSbyc8F9Na/9IZLNwJeA=
github.com/google/go-containerregistry v0.1.0 h1:hL5mVw7cTX3SBr64Arpv+cJH93L+Z9Q6WjckImYLB3g=
github.com/google/go-containerregistry v0.1.0/go.mod h1:npTSyywOeILcgWqd+rvtzGWflIPPcBQhYoOONaY4ltM=
github.com/google/go-containerregistry v0.1.1 h1:AG8FSAfXglim2l5qSrqp5VK2Xl03PiBf25NiTGGamws=
github.com/google/go-containerregistry v0.1.1/go.mod h1:npTSyywOeILcgWqd+rvtzGWflIPPcBQhYoOONaY4ltM=
github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo=
github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
@@ -444,9 +444,8 @@ github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1K
github.com/gookit/color v1.2.7 h1:4qePMNWZhrmbfYJDix+J4V2l0iVW+6jQGjicELlN14E=
github.com/gookit/color v1.2.7/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw=
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/goreleaser/goreleaser v0.136.0 h1:Z+7XPrfGK11s/Sp+a06sx2FzGuCjTBdxN2ubpGvQbjY=
github.com/goreleaser/goreleaser v0.136.0/go.mod h1:wiKrPUeSNh6Wu8nUHxZydSOVQ/OZvOaO7DTtFqie904=
github.com/goreleaser/nfpm v1.2.1/go.mod h1:TtWrABZozuLOttX2uDlYyECfQX7x5XYkVxhjYcR6G9w=
@@ -667,7 +666,7 @@ github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
github.com/package-url/packageurl-go v0.1.0 h1:efWBc98O/dBZRg1pw2xiDzovnlMjCa9NPnfaiBduh8I=
github.com/package-url/packageurl-go v0.1.0/go.mod h1:C/ApiuWpmbpni4DIOECf6WCjFUZV7O1Fx7VAzrZHgBw=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -740,7 +739,6 @@ github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
@@ -766,6 +764,8 @@ github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905 h1:Xn3y6St5vlEgvxekK6JuArarc79GKAj/jbcsMRCaRH4=
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905/go.mod h1:jCpPr/pv3OLY1D1nS9lN5HIdIpmOO/WTYJcAmnPq1nU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
@@ -788,6 +788,7 @@ github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRci
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -820,8 +821,8 @@ github.com/ultraware/funlen v0.0.2 h1:Av96YVBwwNSe4MLR7iI/BIa3VyI7/djnto/pK3Uxbd
github.com/ultraware/funlen v0.0.2/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA=
github.com/ultraware/whitespace v0.0.4 h1:If7Va4cM03mpgrNH9k49/VOicWpGoG70XPBFFODYDsg=
github.com/ultraware/whitespace v0.0.4/go.mod h1:aVMh/gQve5Maj9hQ/hg+F75lr/X5A89uZnzAmWSineA=
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/uudashr/gocognit v1.0.1 h1:MoG2fZ0b/Eo7NXoIwCVFLG5JED3qgQz5/NEE+rOsjPs=
github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3cQMJ5JUM=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
@@ -959,11 +960,10 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 h1:eDrdRpKgkcCqKZQwyZRyeFZgfqt37SL7Kv3tok06cKE=
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -980,8 +980,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1000,7 +998,6 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190620070143-6f217b454f45/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1012,7 +1009,7 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191112214154-59a1497f0cea/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1020,6 +1017,7 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1028,14 +1026,11 @@ golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o=
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200610111108-226ff32320da h1:bGb80FudwxpeucJUjPYJXuJ8Hk91vNtfvrymzwiei38=
golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1098,7 +1093,6 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200204192400-7124308813f3/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200210192313-1ace956b0e17/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -1174,20 +1168,17 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200519141106-08726f379972/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece h1:1YM0uhfumvoDu9sx8+RyWwTI63zoCQvI23IYFRlvte0=
google.golang.org/genproto v0.0.0-20200527145253-8367513e4ece/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200604104852-0b0486081ffb h1:ek2py5bOqzR7MR/6obzk0rXUgYCLmjyLnaO9ssT+l6w=
google.golang.org/genproto v0.0.0-20200604104852-0b0486081ffb/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/genproto v0.0.0-20200615140333-fd031eab31e7 h1:1N7l1PuXZwEK7OhHdmKQROOM75PnUjABGwvVRbLBgFk=
google.golang.org/genproto v0.0.0-20200615140333-fd031eab31e7/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
@@ -1223,8 +1214,6 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.56.0 h1:DPMeDvGTM54DXbPkVIZsp19fp/I2K7zwA/itHYHKo8Y=
gopkg.in/ini.v1 v1.56.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
+14
View File
@@ -0,0 +1,14 @@
package file
import (
"io"
"os"
)
type Opener struct {
path string
}
func (o Opener) Open() (io.ReadCloser, error) {
return os.Open(o.path)
}
@@ -1,4 +1,7 @@
#!/usr/bin/env bash
set -eux
zip -r "$1" zip-source
# $1 —— absolute path to destination file, should end with .zip, ideally
# $2 —— absolute path to directory from which to add entries to the archive
pushd "$2" && find . -print | zip "$1" -@ && popd
+131
View File
@@ -0,0 +1,131 @@
package file
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"path/filepath"
"syscall"
"testing"
)
var expectedZipArchiveEntries = []string{
"some-dir" + string(os.PathSeparator),
filepath.Join("some-dir", "a-file.txt"),
"b-file.txt",
"nested.zip",
}
// fatalIfError calls the supplied function. If the function returns a non-nil error, t.Fatal(err) is called.
func fatalIfError(t *testing.T, fn func() error) {
t.Helper()
if fn == nil {
return
}
err := fn()
if err != nil {
t.Fatal(err)
}
}
// createZipArchive creates a new ZIP archive file at destinationArchivePath based on the directory found at
// sourceDirPath.
func createZipArchive(t *testing.T, sourceDirPath, destinationArchivePath string) error {
t.Helper()
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("unable to get cwd: %+v", err)
}
cmd := exec.Command("./generate-zip-fixture.sh", destinationArchivePath, path.Base(sourceDirPath))
cmd.Dir = filepath.Join(cwd, "test-fixtures")
if err := cmd.Start(); err != nil {
return fmt.Errorf("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 {
return fmt.Errorf("failed to generate fixture: rc=%d", status.ExitStatus())
}
}
} else {
return fmt.Errorf("unable to get generate fixture script result: %+v", err)
}
}
return nil
}
// setupZipFileTest encapsulates common test setup work for zip file tests. It returns a cleanup function,
// which should be called (typically deferred) by the caller, the path of the created zip archive, and an error,
// which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
// (even if there's an error), and it should always be called.
func setupZipFileTest(t *testing.T, sourceDirPath string) (func() error, string, error) {
t.Helper()
// Keep track of any needed cleanup work as we go
var cleanupFns []func() error
cleanup := func(fns []func() error) func() error {
return func() error {
for _, fn := range fns {
err := fn()
if err != nil {
return err
}
}
return nil
}
}
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
if err != nil {
return cleanup(cleanupFns), "", fmt.Errorf("unable to create tempfile: %+v", err)
}
cleanupFns = append(cleanupFns, func() error { return os.Remove(archivePrefix.Name()) })
destinationArchiveFilePath := archivePrefix.Name() + ".zip"
t.Logf("archive path: %s", destinationArchiveFilePath)
err = createZipArchive(t, sourceDirPath, destinationArchiveFilePath)
cleanupFns = append(cleanupFns, func() error { return os.Remove(destinationArchiveFilePath) })
if err != nil {
return cleanup(cleanupFns), "", err
}
cwd, err := os.Getwd()
if err != nil {
return cleanup(cleanupFns), "", fmt.Errorf("unable to get cwd: %+v", err)
}
t.Logf("running from: %s", cwd)
return cleanup(cleanupFns), destinationArchiveFilePath, nil
}
// TODO: Consider moving any non-git asset generation to a task (e.g. make) that's run ahead of running go tests.
func ensureNestedZipExists(t *testing.T, sourceDirPath string) error {
t.Helper()
nestedArchiveFilePath := path.Join(sourceDirPath, "nested.zip")
err := createZipArchive(t, sourceDirPath, nestedArchiveFilePath)
if err != nil {
return fmt.Errorf("unable to create nested archive for test fixture: %+v", err)
}
return nil
}
+15 -2
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"os"
"sort"
"strings"
"github.com/anchore/syft/internal"
@@ -26,7 +27,11 @@ func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
for _, pattern := range patterns {
for entry := range z {
if GlobMatch(pattern, entry) {
// We want to match globs as if entries begin with a leading slash (akin to an absolute path)
// so that glob logic is consistent inside and outside of ZIP archives
normalizedEntry := normalizeZipEntryName(entry)
if GlobMatch(pattern, normalizedEntry) {
uniqueMatches.Add(entry)
}
}
@@ -47,7 +52,7 @@ func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
defer func() {
err = zipReader.Close()
if err != nil {
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
log.Errorf("unable to close zip archive (%s): %+v", archivePath, err)
}
}()
@@ -56,3 +61,11 @@ func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
}
return manifest, nil
}
func normalizeZipEntryName(entry string) string {
if !strings.HasPrefix(entry, "/") {
return "/" + entry
}
return entry
}
+109
View File
@@ -0,0 +1,109 @@
package file
import (
"encoding/json"
"os"
"path"
"testing"
)
func TestNewZipFileManifest(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
sourceDirPath := path.Join(cwd, "test-fixtures", "zip-source")
err = ensureNestedZipExists(t, sourceDirPath)
if err != nil {
t.Fatal(err)
}
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
defer fatalIfError(t, cleanup)
if err != nil {
t.Fatal(err)
}
actual, err := NewZipFileManifest(archiveFilePath)
if err != nil {
t.Fatalf("unable to extract from unzip archive: %+v", err)
}
if len(expectedZipArchiveEntries) != len(actual) {
t.Fatalf("mismatched manifest: %d != %d", len(actual), len(expectedZipArchiveEntries))
}
for _, e := range expectedZipArchiveEntries {
_, 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))
}
}
func TestZipFileManifest_GlobMatch(t *testing.T) {
cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
sourceDirPath := path.Join(cwd, "test-fixtures", "zip-source")
err = ensureNestedZipExists(t, sourceDirPath)
if err != nil {
t.Fatal(err)
}
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
//goland:noinspection GoNilness
defer fatalIfError(t, cleanup)
if err != nil {
t.Fatal(err)
}
z, err := NewZipFileManifest(archiveFilePath)
if err != nil {
t.Fatalf("unable to extract from unzip archive: %+v", err)
}
cases := []struct {
glob string
expected string
}{
{
"/b*",
"b-file.txt",
},
{
"*/a-file.txt",
"some-dir/a-file.txt",
},
{
"**/*.zip",
"nested.zip",
},
}
for _, tc := range cases {
t.Run(tc.glob, func(t *testing.T) {
glob := tc.glob
results := z.GlobMatch(glob)
if len(results) == 1 && results[0] == tc.expected {
return
}
t.Errorf("unexpected results for glob '%s': %+v", glob, results)
})
}
}
+14 -8
View File
@@ -43,7 +43,7 @@ func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths
defer func() {
err = zipReader.Close()
if err != nil {
log.Errorf("unable to close zip archive (%s): %w", archivePath, err)
log.Errorf("unable to close zip archive (%s): %+v", archivePath, err)
}
}()
@@ -63,8 +63,8 @@ func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths
return nil
}
func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (map[string]io.Reader, error) {
results := make(map[string]io.Reader)
func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (map[string]Opener, error) {
results := make(map[string]Opener)
// don't allow for full traversal, only select traversal from given paths
if len(paths) == 0 {
@@ -78,11 +78,21 @@ func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (m
if err != nil {
return fmt.Errorf("unable to create temp file: %w", err)
}
// we shouldn't try and keep the tempfile open as the returned result may have several files, which takes up
// resources (leading to "too many open files"). Instead we'll return a file opener to the caller which
// provides a ReadCloser. It is up to the caller to handle closing the file explicitly.
defer tempFile.Close()
zippedFile, err := file.Open()
if err != nil {
return fmt.Errorf("unable to read file=%q from zip=%q: %w", file.Name, archivePath, err)
}
defer func() {
err := zippedFile.Close()
if err != nil {
log.Errorf("unable to close source file=%q from zip=%q: %+v", file.Name, archivePath, err)
}
}()
if file.FileInfo().IsDir() {
return fmt.Errorf("unable to extract directories, only files: %s", file.Name)
@@ -103,12 +113,8 @@ func ExtractFromZipToUniqueTempFile(archivePath, dir string, paths ...string) (m
return fmt.Errorf("unable to reset file pointer (%s): %w", tempFile.Name(), err)
}
results[file.Name] = tempFile
results[file.Name] = Opener{path: tempFile.Name()}
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
}
+31 -122
View File
@@ -6,47 +6,14 @@ import (
"io"
"io/ioutil"
"os"
"os/exec"
"path"
"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()
@@ -67,54 +34,50 @@ func equal(r1, r2 io.Reader) (bool, error) {
}
func TestUnzipToDir(t *testing.T) {
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("unable to create tempfile: %+v", err)
t.Fatal(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())
goldenRootDir := filepath.Join(cwd, "test-fixtures")
sourceDirPath := path.Join(goldenRootDir, "zip-source")
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
defer fatalIfError(t, cleanup)
if err != nil {
t.Fatal(err)
}
contentsDir, err := ioutil.TempDir("", "syft-ziputil-contents-TEST-")
unzipDestinationDir, err := ioutil.TempDir("", "syft-ziputil-contents-TEST-")
defer os.RemoveAll(unzipDestinationDir)
if err != nil {
t.Fatalf("unable to create tempdir: %+v", err)
}
defer os.RemoveAll(contentsDir)
t.Logf("content path: %s", contentsDir)
t.Logf("content path: %s", unzipDestinationDir)
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
expectedPaths := len(expectedZipArchiveEntries)
observedPaths := 0
err = UnzipToDir(archivePath, contentsDir)
err = UnzipToDir(archiveFilePath, unzipDestinationDir)
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"),
err = filepath.Walk(unzipDestinationDir,
func(path string, info os.FileInfo, err error) error {
t.Logf("unzipped path: %s", path)
observedPaths++
// We don't unzip the root archive dir, since there's no archive entry for it
if path != unzipDestinationDir {
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))
goldenPath := filepath.Join(sourceDirPath, strings.TrimPrefix(path, unzipDestinationDir))
if info.IsDir() {
i, err := os.Stat(goldenPath)
@@ -156,12 +119,11 @@ func TestUnzipToDir(t *testing.T) {
}
if observedPaths != expectedPaths {
t.Errorf("missed test paths: %d!=%d", observedPaths, expectedPaths)
t.Errorf("missed test paths: %d != %d", observedPaths, expectedPaths)
}
}
func TestExtractFilesFromZipFile(t *testing.T) {
func TestContentsFromZip(t *testing.T) {
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
if err != nil {
t.Fatalf("unable to create tempfile: %+v", err)
@@ -172,7 +134,10 @@ func TestExtractFilesFromZipFile(t *testing.T) {
defer os.Remove(archivePath)
t.Logf("archive path: %s", archivePath)
generateFixture(t, archivePrefix.Name())
err = createZipArchive(t, "zip-source", archivePrefix.Name())
if err != nil {
t.Fatal(err)
}
cwd, err := os.Getwd()
if err != nil {
@@ -181,8 +146,8 @@ func TestExtractFilesFromZipFile(t *testing.T) {
t.Logf("running from: %s", cwd)
aFilePath := filepath.Join("zip-source", "some-dir", "a-file.txt")
bFilePath := filepath.Join("zip-source", "b-file.txt")
aFilePath := filepath.Join("some-dir", "a-file.txt")
bFilePath := filepath.Join("b-file.txt")
expected := map[string]string{
aFilePath: "A file! nice!",
@@ -207,60 +172,4 @@ func TestExtractFilesFromZipFile(t *testing.T) {
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))
}
}
+15
View File
@@ -0,0 +1,15 @@
package internal
import "regexp"
// MatchCaptureGroups takes a regular expression and string and returns all of the named capture group results in a map.
func MatchCaptureGroups(regEx *regexp.Regexp, str string) map[string]string {
match := regEx.FindStringSubmatch(str)
results := make(map[string]string)
for i, name := range regEx.SubexpNames() {
if i > 0 && i <= len(match) {
results[name] = match[i]
}
}
return results
}
+7 -7
View File
@@ -16,13 +16,13 @@ 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
Version string `json:"version"` // application semantic version
GitCommit string `json:"gitCommit"` // git SHA at build-time
GitTreeState string `json:"gitTreeState"` // indication of git tree (either "clean" or "dirty") at build-time
BuildDate string `json:"buildDate"` // date of the build
GoVersion string `json:"goVersion"` // go runtime version at build-time
Compiler string `json:"compiler"` // compiler used at build-time
Platform string `json:"platform"` // GOOS and GOARCH at build-time
}
// FromBuild provides all version details
-5
View File
@@ -1,5 +0,0 @@
#!/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
+9
View File
@@ -0,0 +1,9 @@
## Updating the JSON schema
Today the JSON schema is generated from integration test data. Specifically, when integration tests are run, the `/schema/json/examples` directory is populated with syft JSON output data. This examples directory is used to drive automatically generating the JSON schema.
The caveats with this approach is:
1) the JSON schema is only as good as the examples provided
2) there is an integration test that ensures that the JSON schema is valid relative to what the code currently generates.
This means to update the JSON schema you need to
1) Open up `test/integration/json_schema_test.go` and comment out invocations of the `validateAgainstV1Schema` function.
2) From the root of the repo run `generate-json-schema`. Now there should be a new schema generated at `/schema/json/schema.json`
3) Uncomment the `validateAgainstV1Schema` function.
+223 -364
View File
@@ -4,7 +4,7 @@
"artifacts": {
"items": {
"properties": {
"found-by": {
"foundBy": {
"items": {
"type": "string"
},
@@ -18,7 +18,7 @@
},
{
"properties": {
"layer-index": {
"layerIndex": {
"type": "integer"
},
"path": {
@@ -26,7 +26,7 @@
}
},
"required": [
"layer-index",
"layerIndex",
"path"
],
"type": "object"
@@ -40,6 +40,12 @@
"architecture": {
"type": "string"
},
"author": {
"type": "string"
},
"authorEmail": {
"type": "string"
},
"description": {
"type": "string"
},
@@ -48,407 +54,243 @@
},
"files": {
"items": {
"properties": {
"checksum": {
"anyOf": [
{
"type": "string"
},
"owner-gid": {
"type": "string"
},
"owner-uid": {
"type": "string"
},
"path": {
"type": "string"
},
"permissions": {
"type": "string"
{
"properties": {
"checksum": {
"type": "string"
},
"digest": {
"properties": {
"algorithm": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"algorithm",
"value"
],
"type": "object"
},
"ownerGid": {
"type": "string"
},
"ownerUid": {
"type": "string"
},
"path": {
"type": "string"
},
"permissions": {
"type": "string"
},
"size": {
"type": "string"
}
},
"required": [
"path"
],
"type": "object"
}
},
"required": [
"checksum",
"owner-gid",
"owner-uid",
"path",
"permissions"
],
"type": "object"
]
},
"type": "array"
},
"git-commit-of-apk-port": {
"gitCommitOfApkPort": {
"type": "string"
},
"installed-size": {
"homepage": {
"type": "string"
},
"installedSize": {
"type": "integer"
},
"license": {
"type": "string"
},
"licenses": {
"items": {
"type": "string"
},
"type": "array"
},
"maintainer": {
"type": "string"
},
"manifest": {
"anyOf": [
{
"type": "null"
},
{
"properties": {
"main": {
"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": {
"Archiver-Version": {
"type": "string"
},
"implementation-vendor": {
"Build-Jdk": {
"type": "string"
},
"implementation-version": {
"Built-By": {
"type": "string"
},
"manifest-version": {
"Created-By": {
"type": "string"
},
"name": {
"Extension-Name": {
"type": "string"
},
"specification-title": {
"Group-Id": {
"type": "string"
},
"specification-vendor": {
"Hudson-Version": {
"type": "string"
},
"specification-version": {
"Implementation-Title": {
"type": "string"
},
"Implementation-Version": {
"type": "string"
},
"Jenkins-Version": {
"type": "string"
},
"Long-Name": {
"type": "string"
},
"Main-Class": {
"type": "string"
},
"Manifest-Version": {
"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"
},
"Specification-Title": {
"type": "string"
}
},
"required": [
"extra-fields",
"implementation-title",
"implementation-vendor",
"implementation-version",
"manifest-version",
"name",
"specification-title",
"specification-vendor",
"specification-version"
"Archiver-Version",
"Build-Jdk",
"Built-By",
"Created-By",
"Manifest-Version"
],
"type": "object"
}
]
},
"required": [
"main"
],
"type": "object"
},
"origin-package": {
"name": {
"type": "string"
},
"originPackage": {
"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"
}
]
"platform": {
"type": "string"
},
"pom-properties": {
"pomProperties": {
"properties": {
"Path": {
"artifactId": {
"type": "string"
},
"artifact-id": {
"type": "string"
},
"extra-fields": {
"extraFields": {
"type": "null"
},
"group-id": {
"groupId": {
"type": "string"
},
"name": {
"type": "string"
},
"path": {
"type": "string"
},
"version": {
"type": "string"
}
},
"required": [
"Path",
"artifact-id",
"extra-fields",
"group-id",
"artifactId",
"extraFields",
"groupId",
"name",
"path",
"version"
],
"type": "object"
},
"pull-checksum": {
"pullChecksum": {
"type": "string"
},
"pull-dependencies": {
"pullDependencies": {
"type": "string"
},
"release": {
"type": "string"
},
"sitePackagesRootPath": {
"type": "string"
},
"size": {
"type": "integer"
},
"source": {
"type": "string"
},
"sourceRpm": {
"type": "string"
},
"topLevelPackages": {
"items": {
"type": "string"
},
"type": "array"
},
"url": {
"type": "string"
},
"vendor": {
"type": "string"
},
"version": {
"type": "string"
},
"virtualPath": {
"type": "string"
}
},
"type": "object"
@@ -464,7 +306,7 @@
}
},
"required": [
"found-by",
"foundBy",
"locations",
"name",
"type",
@@ -474,61 +316,78 @@
},
"type": "array"
},
"directory": {
"type": "string"
},
"image": {
"source": {
"properties": {
"digest": {
"type": "string"
},
"layers": {
"items": {
"properties": {
"digest": {
"type": "string"
},
"media-type": {
"type": "string"
},
"size": {
"type": "integer"
}
"target": {
"anyOf": [
{
"type": "string"
},
"required": [
"digest",
"media-type",
"size"
],
"type": "object"
},
"type": "array"
{
"properties": {
"digest": {
"type": "string"
},
"layers": {
"items": {
"properties": {
"digest": {
"type": "string"
},
"mediaType": {
"type": "string"
},
"size": {
"type": "integer"
}
},
"required": [
"digest",
"mediaType",
"size"
],
"type": "object"
},
"type": "array"
},
"mediaType": {
"type": "string"
},
"size": {
"type": "integer"
},
"tags": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"digest",
"layers",
"mediaType",
"size",
"tags"
],
"type": "object"
}
]
},
"media-type": {
"type": {
"type": "string"
},
"size": {
"type": "integer"
},
"tags": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"digest",
"layers",
"media-type",
"size",
"tags"
"target",
"type"
],
"type": "object"
}
},
"required": [
"artifacts"
"artifacts",
"source"
],
"type": "object"
}
+3 -28
View File
@@ -4,39 +4,14 @@ Package apkdb provides a concrete Cataloger implementation for Alpine DB files.
package apkdb
import (
"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 {
// NewApkdbCataloger returns a new Alpine DB cataloger object.
func NewApkdbCataloger() *common.GenericCataloger {
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 (a *Cataloger) Name() string {
return "apkdb-cataloger"
}
// SelectFiles returns a set of discovered Alpine DB files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver)
}
// Catalog returns the Packages indexed from all Alpine DB files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name())
return common.NewGenericCataloger(nil, globParsers, "apkdb-cataloger")
}
+14 -6
View File
@@ -20,9 +20,14 @@ 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) {
// larger capacity for the scanner.
const maxScannerCapacity = 1024 * 1024
// a new larger buffer for the scanner
bufScan := make([]byte, maxScannerCapacity)
packages := make([]pkg.Package, 0)
scanner := bufio.NewScanner(reader)
scanner.Buffer(bufScan, maxScannerCapacity)
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' {
@@ -44,11 +49,12 @@ func parseApkDB(_ string, reader io.Reader) ([]pkg.Package, error) {
}
if metadata != nil {
packages = append(packages, pkg.Package{
Name: metadata.Package,
Version: metadata.Version,
Licenses: strings.Split(metadata.License, " "),
Type: pkg.ApkPkg,
Metadata: *metadata,
Name: metadata.Package,
Version: metadata.Version,
Licenses: strings.Split(metadata.License, " "),
Type: pkg.ApkPkg,
MetadataType: pkg.ApkMetadataType,
Metadata: *metadata,
})
}
}
@@ -93,7 +99,7 @@ func parseApkDBEntry(reader io.Reader) (*pkg.ApkMetadata, error) {
fileRecord = &files[len(files)-1]
case "a":
ownershipFields := strings.Split(value, ":")
if len(ownershipFields) != 3 {
if len(ownershipFields) < 3 {
log.Errorf("unexpected APK ownership field: %q", value)
continue
}
@@ -104,6 +110,8 @@ func parseApkDBEntry(reader io.Reader) (*pkg.ApkMetadata, error) {
fileRecord.OwnerUID = ownershipFields[0]
fileRecord.OwnerGUI = ownershipFields[1]
fileRecord.Permissions = ownershipFields[2]
// note: there are more optional fields available that we are not capturing, e.g.:
// "0:0:755:Q1JaDEHQHBbizhEzoWK1YxuraNU/4="
case "Z":
if fileRecord == nil {
log.Errorf("checksum field with no parent record: %q", value)
+60 -8
View File
@@ -10,6 +10,56 @@ import (
"github.com/anchore/syft/syft/pkg"
)
func TestExtraFileAttributes(t *testing.T) {
tests := []struct {
name string
expected pkg.ApkMetadata
}{
{
name: "test extra file attributes (checksum) are ignored",
expected: pkg.ApkMetadata{
Files: []pkg.ApkFileRecord{
{
Path: "/usr/lib/jvm/java-1.8-openjdk/bin/policytool",
OwnerUID: "0",
OwnerGUI: "0",
Permissions: "755",
Checksum: "Q1M0C9qfC/+kdRiOodeihG2GMRtkE=",
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
file, err := os.Open("test-fixtures/extra-file-attributes")
if err != nil {
t.Fatal("Unable to read test-fixtures/extra-file-attributes: ", 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.Files, test.expected.Files); diff != nil {
for _, d := range diff {
t.Errorf("diff: %+v", d)
}
}
})
}
}
func TestSinglePackage(t *testing.T) {
tests := []struct {
name string
@@ -110,10 +160,11 @@ func TestMultiplePackages(t *testing.T) {
fixture: "test-fixtures/multiple",
expected: []pkg.Package{
{
Name: "libc-utils",
Version: "0.7.2-r0",
Licenses: []string{"BSD"},
Type: pkg.ApkPkg,
Name: "libc-utils",
Version: "0.7.2-r0",
Licenses: []string{"BSD"},
Type: pkg.ApkPkg,
MetadataType: pkg.ApkMetadataType,
Metadata: pkg.ApkMetadata{
Package: "libc-utils",
OriginPackage: "libc-dev",
@@ -132,10 +183,11 @@ func TestMultiplePackages(t *testing.T) {
},
},
{
Name: "musl-utils",
Version: "1.1.24-r2",
Licenses: []string{"MIT", "BSD", "GPL2+"},
Type: pkg.ApkPkg,
Name: "musl-utils",
Version: "1.1.24-r2",
Licenses: []string{"MIT", "BSD", "GPL2+"},
Type: pkg.ApkPkg,
MetadataType: pkg.ApkMetadataType,
Metadata: pkg.ApkMetadata{
Package: "musl-utils",
OriginPackage: "musl",
@@ -0,0 +1,22 @@
P:openjdk8-jre
V:8.212.04-r0
A:x86_64
S:359029
I:970752
T:OpenJDK 8 Java Runtime
U:https://icedtea.classpath.org/
L:custom
o:openjdk8
m:Timo Teras <timo.teras@iki.fi>
t:1556993127
c:46badd1aabd3375ae4b41c7194bc448f74b2ea4a
D:java-cacerts nss so:libX11.so.6 so:libXcomposite.so.1 so:libXext.so.6 so:libXi.so.6 so:libXrender.so.1 so:libXtst.so.6 so:libasound.so.2 so:libc.musl-x86_64.so.1 so:libfreetype.so.6 so:libgcc_s.so.1 so:libgif.so.7 so:libjpeg.so.8 so:libpng16.so.16 so:libstdc++.so.6 so:openjdk8:libawt.so so:openjdk8:libjava.so so:openjdk8:libjli.so so:openjdk8:libjvm.so
p:so:openjdk8:libawt_xawt.so=0 so:openjdk8:libfontmanager.so=0 so:openjdk8:libjawt.so=0 so:openjdk8:libjsoundalsa.so=0 so:openjdk8:libsplashscreen.so=0
F:usr
F:usr/lib
F:usr/lib/jvm
F:usr/lib/jvm/java-1.8-openjdk
F:usr/lib/jvm/java-1.8-openjdk/bin
R:policytool
a:0:0:755:Q1JaDEHQHBbizhEzoWK1YxuraNU/4=
Z:Q1M0C9qfC/+kdRiOodeihG2GMRtkE=
-42
View File
@@ -1,42 +0,0 @@
/*
Package bundler provides a concrete Cataloger implementation for Ruby Gemfile.lock bundler files.
*/
package bundler
import (
"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 (a *Cataloger) Name() string {
return "bundler-cataloger"
}
// SelectFiles returns a set of discovered Gemfile.lock files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver)
}
// Catalog returns the Packages indexed from all Gemfile.lock files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name())
}
@@ -1,100 +0,0 @@
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)
}
}
}
+6 -22
View File
@@ -1,7 +1,6 @@
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"
@@ -39,36 +38,21 @@ func newMonitor() (*progress.Manual, *progress.Manual) {
// 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 {
fileSelection = append(fileSelection, a.SelectFiles(resolver)...)
log.Debugf("cataloger '%s' selected '%d' files", a.Name(), len(fileSelection))
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 _, a := range catalogers {
// TODO: check for multiple rounds of analyses by Iterate error
packages, err := a.Catalog(contents)
for _, theCataloger := range catalogers {
packages, err := theCataloger.Catalog(resolver)
if err != nil {
errs = multierror.Append(errs, err)
continue
}
log.Debugf("cataloger '%s' discovered '%d' packages", a.Name(), len(packages))
packagesDiscovered.N += int64(len(packages))
catalogedPackages := len(packages)
log.Debugf("cataloger '%s' discovered '%d' packages", theCataloger.Name(), catalogedPackages)
packagesDiscovered.N += int64(catalogedPackages)
for _, p := range packages {
catalog.Add(p)
+29 -19
View File
@@ -6,15 +6,14 @@ catalogers defined in child packages as well as the interface definition to impl
package cataloger
import (
"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/dpkg"
"github.com/anchore/syft/syft/cataloger/deb"
"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/cataloger/ruby"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/scope"
)
@@ -25,24 +24,35 @@ import (
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]string) ([]pkg.Package, error)
// TODO: add "IterationNeeded" error to indicate to the driver to continue with another Select/Catalog pass
// TODO: we should consider refactoring to return a set of io.Readers instead of the full contents themselves (allow for optional buffering).
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing the catalog source.
Catalog(resolver scope.Resolver) ([]pkg.Package, error)
}
// All returns a slice of all locally defined catalogers (defined in child packages).
func All() []Cataloger {
// ImageCatalogers returns a slice of locally implemented catalogers that are fit for detecting installations of packages.
func ImageCatalogers() []Cataloger {
return []Cataloger{
dpkg.New(),
bundler.New(),
python.New(),
rpmdb.New(),
java.New(),
apkdb.New(),
golang.New(),
javascript.New(),
ruby.NewGemSpecCataloger(),
python.NewPythonPackageCataloger(),
javascript.NewJavascriptPackageCataloger(),
deb.NewDpkgdbCataloger(),
rpmdb.NewRpmdbCataloger(),
java.NewJavaCataloger(),
apkdb.NewApkdbCataloger(),
golang.NewGoModCataloger(),
}
}
// DirectoryCatalogers returns a slice of locally implemented catalogers that are fit for detecting packages from index files (and select installations)
func DirectoryCatalogers() []Cataloger {
return []Cataloger{
ruby.NewGemFileLockCataloger(),
python.NewPythonIndexCataloger(),
python.NewPythonPackageCataloger(),
javascript.NewJavascriptLockCataloger(),
deb.NewDpkgdbCataloger(),
rpmdb.NewRpmdbCataloger(),
java.NewJavaCataloger(),
apkdb.NewApkdbCataloger(),
golang.NewGoModCataloger(),
}
}
+47 -30
View File
@@ -15,85 +15,102 @@ import (
// 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
globParsers map[string]ParserFn
pathParsers map[string]ParserFn
selectedFiles []file.Reference
parsers map[file.Reference]ParserFn
upstreamCataloger string
}
// 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),
func NewGenericCataloger(pathParsers map[string]ParserFn, globParsers map[string]ParserFn, upstreamCataloger string) *GenericCataloger {
return &GenericCataloger{
globParsers: globParsers,
pathParsers: pathParsers,
selectedFiles: make([]file.Reference, 0),
parsers: make(map[file.Reference]ParserFn),
upstreamCataloger: upstreamCataloger,
}
}
// Name returns a string that uniquely describes the upstream cataloger that this Generic Cataloger represents.
func (c *GenericCataloger) Name() string {
return c.upstreamCataloger
}
// 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...)
func (c *GenericCataloger) register(files []file.Reference, parser ParserFn) {
c.selectedFiles = append(c.selectedFiles, files...)
for _, f := range files {
a.parsers[f] = parser
c.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)
func (c *GenericCataloger) clear() {
c.selectedFiles = make([]file.Reference, 0)
c.parsers = make(map[file.Reference]ParserFn)
}
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing the catalog source.
func (c *GenericCataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) {
fileSelection := c.selectFiles(resolver)
contents, err := resolver.MultipleFileContentsByRef(fileSelection...)
if err != nil {
return nil, err
}
return c.catalog(contents)
}
// 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 {
func (c *GenericCataloger) selectFiles(resolver scope.FileResolver) []file.Reference {
// select by exact path
for path, parser := range a.pathParsers {
for path, parser := range c.pathParsers {
files, err := resolver.FilesByPath(file.Path(path))
if err != nil {
log.Errorf("cataloger failed to select files by path: %w", err)
log.Errorf("cataloger failed to select files by path: %+v", err)
}
if files != nil {
a.register(files, parser)
c.register(files, parser)
}
}
// select by glob pattern
for globPattern, parser := range a.globParsers {
for globPattern, parser := range c.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)
c.register(fileMatches, parser)
}
}
return a.selectedFiles
return c.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]string, upstreamMatcher string) ([]pkg.Package, error) {
defer a.clear()
// catalog takes a set of file contents and uses any configured parser functions to resolve and return discovered packages
func (c *GenericCataloger) catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
defer c.clear()
packages := make([]pkg.Package, 0)
for reference, parser := range a.parsers {
for reference, parser := range c.parsers {
content, ok := contents[reference]
if !ok {
log.Errorf("cataloger '%s' missing file content: %+v", upstreamMatcher, reference)
log.Errorf("cataloger '%s' missing file content: %+v", c.upstreamCataloger, reference)
continue
}
entries, err := parser(string(reference.Path), strings.NewReader(content))
if err != nil {
// TODO: should we fail? or only log?
log.Errorf("cataloger '%s' failed to parse entries (reference=%+v): %w", upstreamMatcher, reference, err)
log.Errorf("cataloger '%s' failed to parse entries (reference=%+v): %+v", c.upstreamCataloger, reference, err)
continue
}
for _, entry := range entries {
entry.FoundBy = upstreamMatcher
entry.FoundBy = c.upstreamCataloger
entry.Source = []file.Reference{reference}
packages = append(packages, entry)
+26 -31
View File
@@ -7,21 +7,28 @@ import (
"testing"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/pkg"
)
type testResolver struct {
type testResolverMock struct {
contents map[file.Reference]string
}
func newTestResolver() *testResolver {
return &testResolver{
func newTestResolver() *testResolverMock {
return &testResolverMock{
contents: make(map[file.Reference]string),
}
}
func (r *testResolver) FilesByPath(paths ...file.Path) ([]file.Reference, error) {
func (r *testResolverMock) FileContentsByRef(_ file.Reference) (string, error) {
return "", fmt.Errorf("not implemented")
}
func (r *testResolverMock) MultipleFileContentsByRef(_ ...file.Reference) (map[file.Reference]string, error) {
return r.contents, nil
}
func (r *testResolverMock) FilesByPath(paths ...file.Path) ([]file.Reference, error) {
results := make([]file.Reference, len(paths))
for idx, p := range paths {
@@ -32,13 +39,17 @@ func (r *testResolver) FilesByPath(paths ...file.Path) ([]file.Reference, error)
return results, nil
}
func (r *testResolver) FilesByGlob(patterns ...string) ([]file.Reference, error) {
func (r *testResolverMock) FilesByGlob(_ ...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 (r *testResolverMock) RelativeFileByPath(_ file.Reference, _ string) (*file.Reference, error) {
return nil, fmt.Errorf("not implemented")
}
func parser(_ string, reader io.Reader) ([]pkg.Package, error) {
contents, err := ioutil.ReadAll(reader)
if err != nil {
@@ -60,36 +71,20 @@ func TestGenericCataloger(t *testing.T) {
"/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
}
upstream := "some-other-cataloger"
expectedPkgs := make(map[file.Reference]pkg.Package)
for path, ref := range selectionByPath {
expectedPkgs[ref] = pkg.Package{
resolver := newTestResolver()
cataloger := NewGenericCataloger(pathParsers, globParsers, upstream)
expectedSelection := []string{"/last/path.txt", "/another-path.txt", "/a-path.txt"}
expectedPkgs := make(map[string]pkg.Package)
for _, path := range expectedSelection {
expectedPkgs[path] = pkg.Package{
FoundBy: upstream,
Source: []file.Reference{ref},
Name: fmt.Sprintf("%s file contents!", path),
}
}
actualPkgs, err := cataloger.Catalog(resolver.contents, upstream)
actualPkgs, err := cataloger.Catalog(resolver)
if err != nil {
t.Fatalf("cataloger catalog action failed: %+v", err)
}
@@ -100,7 +95,7 @@ func TestGenericCataloger(t *testing.T) {
for _, p := range actualPkgs {
ref := p.Source[0]
exP, ok := expectedPkgs[ref]
exP, ok := expectedPkgs[string(ref.Path)]
if !ok {
t.Errorf("missing expected pkg: ref=%+v", ref)
continue
+17
View File
@@ -0,0 +1,17 @@
/*
Package dpkg provides a concrete Cataloger implementation for Debian package DB status files.
*/
package deb
import (
"github.com/anchore/syft/syft/cataloger/common"
)
// NewDpkgdbCataloger returns a new Deb package cataloger object.
func NewDpkgdbCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/var/lib/dpkg/status": parseDpkgStatus,
}
return common.NewGenericCataloger(nil, globParsers, "dpkgdb-cataloger")
}
@@ -1,4 +1,4 @@
package dpkg
package deb
import (
"bufio"
@@ -30,10 +30,11 @@ func parseDpkgStatus(_ string, reader io.Reader) ([]pkg.Package, error) {
return nil, err
}
packages = append(packages, pkg.Package{
Name: entry.Package,
Version: entry.Version,
Type: pkg.DebPkg,
Metadata: entry,
Name: entry.Package,
Version: entry.Version,
Type: pkg.DebPkg,
MetadataType: pkg.DpkgMetadataType,
Metadata: entry,
})
}
@@ -1,4 +1,4 @@
package dpkg
package deb
import (
"bufio"
-42
View File
@@ -1,42 +0,0 @@
/*
Package dpkg provides a concrete Cataloger implementation for Debian package DB status files.
*/
package dpkg
import (
"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 (a *Cataloger) Name() string {
return "dpkg-cataloger"
}
// SelectFiles returns a set of discovered DPKG status files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver)
}
// Catalog returns the Packages indexed from all DPKG status files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name())
}
+3 -28
View File
@@ -4,39 +4,14 @@ Package golang provides a concrete Cataloger implementation for go.mod files.
package golang
import (
"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.GoModulePkg Package Types defined in go.mod files.
type Cataloger struct {
cataloger common.GenericCataloger
}
// New returns a new Go module cataloger object.
func New() *Cataloger {
// NewGoModCataloger returns a new Go module cataloger object.
func NewGoModCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/go.mod": parseGoMod,
}
return &Cataloger{
cataloger: common.NewGenericCataloger(nil, globParsers),
}
}
// Name returns a string that uniquely describes this cataloger.
func (a *Cataloger) Name() string {
return "go-cataloger"
}
// SelectFiles returns a set of discovered go.mod files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver)
}
// Catalog returns the Packages indexed from all go.mod files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name())
return common.NewGenericCataloger(nil, globParsers, "go-cataloger")
}
+1 -2
View File
@@ -6,9 +6,8 @@ import (
"io/ioutil"
"sort"
"github.com/rogpeppe/go-internal/modfile"
"github.com/anchore/syft/syft/pkg"
"golang.org/x/mod/modfile"
)
// parseGoMod takes a go.mod and lists all packages discovered.
+14 -7
View File
@@ -10,11 +10,20 @@ import (
"github.com/anchore/syft/syft/pkg"
)
// match on versions and anything after the version. This is used to isolate the name from the version.
// match examples:
// wagon-webdav-1.0.2-rc1-hudson.jar ---> -1.0.2-rc1-hudson.jar
// windows-remote-command-1.0.jar ---> -1.0.jar
// wstx-asl-1-2.jar ---> -1-2.jar
// guava-rc0.jar ---> -rc0.jar
var versionAreaPattern = regexp.MustCompile(`-(?P<version>(\d+\.)?(\d+\.)?(r?c?\d+)(-[a-zA-Z0-9\-.]+)*)(?P<remaining>.*)$`)
// match on explicit versions. This is used for extracting version information.
// match examples:
// pkg-extra-field-4.3.2-rc1 --> match(name=pkg-extra-field version=4.3.2-rc1)
// pkg-extra-field-4.3-rc1 --> match(name=pkg-extra-field version=4.3-rc1)
// pkg-extra-field-4.3 --> match(name=pkg-extra-field version=4.3)
var versionPattern = regexp.MustCompile(`(?P<name>.+)-(?P<version>(\d+\.)?(\d+\.)?(\*|\d+)(-[a-zA-Z0-9\-\.]+)*)`)
var versionPattern = regexp.MustCompile(`-(?P<version>(\d+\.)?(\d+\.)?(r?c?\d+)(-[a-zA-Z0-9\-.]+)*)`)
type archiveFilename struct {
raw string
@@ -70,10 +79,8 @@ func (a archiveFilename) version() string {
}
func (a archiveFilename) name() string {
// there should be only one name, if there is more or less then something is wrong
if len(a.fields) != 1 {
return ""
}
return a.fields[0]["name"]
// derive the name from the archive name (no path or extension) and remove any versions found
basename := filepath.Base(a.raw)
cleaned := strings.TrimSuffix(basename, filepath.Ext(basename))
return versionAreaPattern.ReplaceAllString(cleaned, "")
}
+68 -1
View File
@@ -1,9 +1,10 @@
package java
import (
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/sergi/go-diff/diffmatchpatch"
"testing"
)
func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
@@ -56,12 +57,78 @@ func TestExtractInfoFromJavaArchiveFilename(t *testing.T) {
name: "pkg-extra-field-maven",
ty: pkg.JenkinsPluginPkg,
},
{
filename: "/some/path-with-version-5.4.3/wagon-webdav-1.0.2-beta-2.2.3a-hudson.jar",
version: "1.0.2-beta-2.2.3a-hudson",
extension: "jar",
name: "wagon-webdav",
ty: pkg.JavaPkg,
},
{
filename: "/some/path-with-version-5.4.3/wagon-webdav-1.0.2-beta-2.2.3-hudson.jar",
version: "1.0.2-beta-2.2.3-hudson",
extension: "jar",
name: "wagon-webdav",
ty: pkg.JavaPkg,
},
{
filename: "/some/path-with-version-5.4.3/windows-remote-command-1.0.jar",
version: "1.0",
extension: "jar",
name: "windows-remote-command",
ty: pkg.JavaPkg,
},
{
filename: "/some/path-with-version-5.4.3/wagon-http-lightweight-1.0.5-beta-2.jar",
version: "1.0.5-beta-2",
extension: "jar",
name: "wagon-http-lightweight",
ty: pkg.JavaPkg,
},
{
filename: "/hudson.war:WEB-INF/lib/commons-jelly-1.1-hudson-20100305.jar",
version: "1.1-hudson-20100305",
extension: "jar",
name: "commons-jelly",
ty: pkg.JavaPkg,
},
{
filename: "/hudson.war:WEB-INF/lib/jtidy-4aug2000r7-dev-hudson-1.jar",
// I don't see how we can reliably account for this case
//version: "4aug2000r7-dev-hudson-1",
version: "",
extension: "jar",
name: "jtidy",
ty: pkg.JavaPkg,
},
{
filename: "/hudson.war:WEB-INF/lib/trilead-ssh2-build212-hudson-5.jar",
// I don't see how we can reliably account for this case
//version: "build212-hudson-5",
version: "5",
extension: "jar",
// name: "trilead-ssh2",
name: "trilead-ssh2-build212-hudson",
ty: pkg.JavaPkg,
},
{
filename: "/hudson.war:WEB-INF/lib/guava-r06.jar",
version: "r06",
extension: "jar",
name: "guava",
ty: pkg.JavaPkg,
},
}
for _, test := range tests {
t.Run(test.filename, func(t *testing.T) {
obj := newJavaArchiveFilename(test.filename)
ty := obj.pkgType()
if ty != test.ty {
t.Errorf("mismatched type: %+v != %v", ty, test.ty)
}
version := obj.version()
if version != test.version {
dmp := diffmatchpatch.New()
+67 -20
View File
@@ -5,6 +5,8 @@ import (
"io"
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/file"
"github.com/anchore/syft/syft/cataloger/common"
@@ -64,13 +66,17 @@ func newJavaArchiveParser(virtualPath string, reader io.Reader, detectNested boo
return nil, cleanupFn, fmt.Errorf("unable to read files from java archive: %w", err)
}
// fetch the last element of the virtual path
virtualElements := strings.Split(virtualPath, ":")
currentFilepath := virtualElements[len(virtualElements)-1]
return &archiveParser{
discoveredPkgs: internal.NewStringSet(),
fileManifest: fileManifest,
virtualPath: virtualPath,
archivePath: archivePath,
contentPath: contentPath,
fileInfo: newJavaArchiveFilename(virtualPath),
fileInfo: newJavaArchiveFilename(currentFilepath),
detectNested: detectNested,
}, cleanupFn, nil
}
@@ -118,7 +124,7 @@ func (j *archiveParser) parse() ([]pkg.Package, error) {
// discoverMainPackage parses the root Java manifest used as the parent package to all discovered nested packages.
func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
// search and parse java manifest files
manifestMatches := j.fileManifest.GlobMatch(manifestPath)
manifestMatches := j.fileManifest.GlobMatch(manifestGlob)
if len(manifestMatches) > 1 {
return nil, fmt.Errorf("found multiple manifests in the jar: %+v", manifestMatches)
} else if len(manifestMatches) == 0 {
@@ -134,24 +140,27 @@ func (j *archiveParser) discoverMainPackage() (*pkg.Package, error) {
// parse the manifest file into a rich object
manifestContents := contents[manifestMatches[0]]
manifest, err := parseJavaManifest(strings.NewReader(manifestContents))
manifest, err := parseJavaManifest(j.archivePath, strings.NewReader(manifestContents))
if err != nil {
return nil, fmt.Errorf("failed to parse java manifest (%s): %w", j.virtualPath, err)
}
return &pkg.Package{
Name: selectName(manifest, j.fileInfo),
Version: selectVersion(manifest, j.fileInfo),
Language: pkg.Java,
Type: pkg.JavaPkg,
Name: selectName(manifest, j.fileInfo),
Version: selectVersion(manifest, j.fileInfo),
Language: pkg.Java,
Type: pkg.JavaPkg,
MetadataType: pkg.JavaMetadataType,
Metadata: pkg.JavaMetadata{
Manifest: manifest,
VirtualPath: j.virtualPath,
Manifest: manifest,
},
}, nil
}
// discoverPkgsFromPomProperties parses Maven POM properties for a given parent package, returning all listed Java packages found and
// associating each discovered package to the given parent package.
// nolint:funlen,gocognit
func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([]pkg.Package, error) {
var pkgs = make([]pkg.Package, 0)
parentKey := uniquePkgKey(parentPkg)
@@ -173,13 +182,22 @@ func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([
if propsObj.Version != "" && propsObj.ArtifactID != "" {
// TODO: if there is no parentPkg (no java manifest) one of these poms could be the parent. We should discover the right parent and attach the correct info accordingly to each discovered package
// keep the artifact name within the virtual path if this package does not match the parent package
vPathSuffix := ""
if !strings.HasPrefix(propsObj.ArtifactID, parentPkg.Name) {
vPathSuffix += ":" + propsObj.ArtifactID
}
virtualPath := j.virtualPath + vPathSuffix
// discovered props = new package
p := pkg.Package{
Name: propsObj.ArtifactID,
Version: propsObj.Version,
Language: pkg.Java,
Type: pkg.JavaPkg,
Name: propsObj.ArtifactID,
Version: propsObj.Version,
Language: pkg.Java,
Type: pkg.JavaPkg,
MetadataType: pkg.JavaMetadataType,
Metadata: pkg.JavaMetadata{
VirtualPath: virtualPath,
PomProperties: propsObj,
Parent: parentPkg,
},
@@ -187,16 +205,35 @@ func (j *archiveParser) discoverPkgsFromPomProperties(parentPkg *pkg.Package) ([
pkgKey := uniquePkgKey(&p)
if !j.discoveredPkgs.Contains(pkgKey) {
// only keep packages we haven't seen yet
pkgs = append(pkgs, p)
} else if pkgKey == parentKey {
// the name/version pair matches...
matchesParentPkg := pkgKey == parentKey
// the virtual path matches...
matchesParentPkg = matchesParentPkg || parentPkg.Metadata.(pkg.JavaMetadata).VirtualPath == virtualPath
// the pom artifactId has the parent name or vice versa
if propsObj.ArtifactID != "" {
matchesParentPkg = matchesParentPkg || strings.Contains(parentPkg.Name, propsObj.ArtifactID) || strings.Contains(propsObj.ArtifactID, parentPkg.Name)
}
if matchesParentPkg {
// we've run across more information about our parent package, add this info to the parent package metadata
// the pom properties is typically a better source of information for name and version than the manifest
if p.Name != parentPkg.Name {
parentPkg.Name = p.Name
}
if p.Version != parentPkg.Version {
parentPkg.Version = p.Version
}
parentMetadata, ok := parentPkg.Metadata.(pkg.JavaMetadata)
if ok {
parentMetadata.PomProperties = propsObj
parentPkg.Metadata = parentMetadata
}
} else if !j.discoveredPkgs.Contains(pkgKey) {
// only keep packages we haven't seen yet (and are not related to the parent package)
pkgs = append(pkgs, p)
}
}
}
@@ -214,18 +251,28 @@ func (j *archiveParser) discoverPkgsFromNestedArchives(parentPkg *pkg.Package) (
}
// search and parse pom.properties files & fetch the contents
readers, err := file.ExtractFromZipToUniqueTempFile(j.archivePath, j.contentPath, j.fileManifest.GlobMatch(archiveFormatGlobs...)...)
openers, err := file.ExtractFromZipToUniqueTempFile(j.archivePath, j.contentPath, j.fileManifest.GlobMatch(archiveFormatGlobs...)...)
if err != nil {
return nil, fmt.Errorf("unable to extract files from zip: %w", err)
}
// discover nested artifacts
for archivePath, archiveReader := range readers {
nestedPath := fmt.Sprintf("%s:%s", j.virtualPath, archivePath)
nestedPkgs, err := parseJavaArchive(nestedPath, archiveReader)
for archivePath, archiveOpener := range openers {
archiveReadCloser, err := archiveOpener.Open()
if err != nil {
return nil, fmt.Errorf("unable to open archived file from tempdir: %w", err)
}
nestedPath := fmt.Sprintf("%s:%s", j.virtualPath, archivePath)
nestedPkgs, err := parseJavaArchive(nestedPath, archiveReadCloser)
if err != nil {
if closeErr := archiveReadCloser.Close(); closeErr != nil {
log.Errorf("unable to close archived file from tempdir: %+v", closeErr)
}
return nil, fmt.Errorf("unable to process nested java archive (%s): %w", archivePath, err)
}
if err = archiveReadCloser.Close(); err != nil {
return nil, fmt.Errorf("unable to close archived file from tempdir: %w", err)
}
// attach the parent package to all discovered packages that are not already associated with a java archive
for _, p := range nestedPkgs {
+80 -99
View File
@@ -78,51 +78,6 @@ func generateJavaBuildFixture(t *testing.T, fixturePath string) {
}
}
func TestSelectName(t *testing.T) {
tests := []struct {
desc string
manifest pkg.JavaManifest
archive archiveFilename
expected string
}{
{
desc: "name from Implementation-Title",
archive: archiveFilename{},
manifest: pkg.JavaManifest{
Name: "",
SpecTitle: "",
ImplTitle: "maven-wrapper",
},
expected: "maven-wrapper",
},
{
desc: "Implementation-Title does not override",
manifest: pkg.JavaManifest{
Name: "Foo",
SpecTitle: "",
ImplTitle: "maven-wrapper",
},
archive: archiveFilename{
fields: []map[string]string{
{"name": "omg"},
},
},
expected: "omg",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
result := selectName(&test.manifest, test.archive)
if result != test.expected {
t.Errorf("mismatch in names: '%s' != '%s'", result, test.expected)
}
})
}
}
func TestParseJar(t *testing.T) {
tests := []struct {
fixture string
@@ -137,17 +92,20 @@ func TestParseJar(t *testing.T) {
},
expected: map[string]pkg.Package{
"example-jenkins-plugin": {
Name: "example-jenkins-plugin",
Version: "1.0-SNAPSHOT",
Language: pkg.Java,
Type: pkg.JenkinsPluginPkg,
Name: "example-jenkins-plugin",
Version: "1.0-SNAPSHOT",
Language: pkg.Java,
Type: pkg.JenkinsPluginPkg,
MetadataType: pkg.JavaMetadataType,
Metadata: pkg.JavaMetadata{
VirtualPath: "test-fixtures/java-builds/packages/example-jenkins-plugin.hpi",
Manifest: &pkg.JavaManifest{
ManifestVersion: "1.0",
SpecTitle: "The Jenkins Plugins Parent POM Project",
ImplTitle: "example-jenkins-plugin",
ImplVersion: "1.0-SNAPSHOT",
Extra: map[string]string{
Main: map[string]string{
"Manifest-Version": "1.0",
"Specification-Title": "The Jenkins Plugins Parent POM Project",
"Implementation-Title": "example-jenkins-plugin",
"Implementation-Version": "1.0-SNAPSHOT",
// extra fields...
"Archiver-Version": "Plexus Archiver",
"Plugin-License-Url": "https://opensource.org/licenses/MIT",
"Plugin-License-Name": "MIT License",
@@ -181,13 +139,17 @@ func TestParseJar(t *testing.T) {
fixture: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar",
expected: map[string]pkg.Package{
"example-java-app-gradle": {
Name: "example-java-app-gradle",
Version: "0.1.0",
Language: pkg.Java,
Type: pkg.JavaPkg,
Name: "example-java-app-gradle",
Version: "0.1.0",
Language: pkg.Java,
Type: pkg.JavaPkg,
MetadataType: pkg.JavaMetadataType,
Metadata: pkg.JavaMetadata{
VirtualPath: "test-fixtures/java-builds/packages/example-java-app-gradle-0.1.0.jar",
Manifest: &pkg.JavaManifest{
ManifestVersion: "1.0",
Main: map[string]string{
"Manifest-Version": "1.0",
},
},
},
},
@@ -200,14 +162,17 @@ func TestParseJar(t *testing.T) {
},
expected: map[string]pkg.Package{
"example-java-app-maven": {
Name: "example-java-app-maven",
Version: "0.1.0",
Language: pkg.Java,
Type: pkg.JavaPkg,
Name: "example-java-app-maven",
Version: "0.1.0",
Language: pkg.Java,
Type: pkg.JavaPkg,
MetadataType: pkg.JavaMetadataType,
Metadata: pkg.JavaMetadata{
VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar",
Manifest: &pkg.JavaManifest{
ManifestVersion: "1.0",
Extra: map[string]string{
Main: map[string]string{
"Manifest-Version": "1.0",
// extra fields...
"Archiver-Version": "Plexus Archiver",
"Created-By": "Apache Maven 3.6.3",
"Built-By": "?",
@@ -224,11 +189,15 @@ func TestParseJar(t *testing.T) {
},
},
"joda-time": {
Name: "joda-time",
Version: "2.9.2",
Language: pkg.Java,
Type: pkg.JavaPkg,
Name: "joda-time",
Version: "2.9.2",
Language: pkg.Java,
Type: pkg.JavaPkg,
MetadataType: pkg.JavaMetadataType,
Metadata: pkg.JavaMetadata{
// ensure that nested packages with different names than that of the parent are appended as
// a suffix on the virtual path
VirtualPath: "test-fixtures/java-builds/packages/example-java-app-maven-0.1.0.jar:joda-time",
PomProperties: &pkg.PomProperties{
Path: "META-INF/maven/joda-time/joda-time/pom.properties",
GroupID: "joda-time",
@@ -295,11 +264,11 @@ func TestParseJar(t *testing.T) {
metadata := a.Metadata.(pkg.JavaMetadata)
metadata.Parent = nil
// ignore select fields
// ignore select fields (only works for the main section)
for _, field := range test.ignoreExtras {
if metadata.Manifest != nil && metadata.Manifest.Extra != nil {
if _, ok := metadata.Manifest.Extra[field]; ok {
delete(metadata.Manifest.Extra, field)
if metadata.Manifest != nil && metadata.Manifest.Main != nil {
if _, ok := metadata.Manifest.Main[field]; ok {
delete(metadata.Manifest.Main, field)
}
}
}
@@ -334,7 +303,7 @@ func TestParseNestedJar(t *testing.T) {
},
{
Name: "spring-boot-starter",
Version: "2.2.2.RELEASE",
Version: "2.2.2",
},
{
Name: "jul-to-slf4j",
@@ -346,7 +315,7 @@ func TestParseNestedJar(t *testing.T) {
},
{
Name: "spring-boot-starter-validation",
Version: "2.2.2.RELEASE",
Version: "2.2.2",
},
{
Name: "hibernate-validator",
@@ -358,7 +327,7 @@ func TestParseNestedJar(t *testing.T) {
},
{
Name: "spring-expression",
Version: "5.2.2.RELEASE",
Version: "5.2.2",
},
{
Name: "jakarta.validation-api",
@@ -366,11 +335,11 @@ func TestParseNestedJar(t *testing.T) {
},
{
Name: "spring-web",
Version: "5.2.2.RELEASE",
Version: "5.2.2",
},
{
Name: "spring-boot-starter-actuator",
Version: "2.2.2.RELEASE",
Version: "2.2.2",
},
{
Name: "log4j-api",
@@ -390,23 +359,23 @@ func TestParseNestedJar(t *testing.T) {
},
{
Name: "spring-aop",
Version: "5.2.2.RELEASE",
Version: "5.2.2",
},
{
Name: "spring-boot-actuator-autoconfigure",
Version: "2.2.2.RELEASE",
Version: "2.2.2",
},
{
Name: "spring-jcl",
Version: "5.2.2.RELEASE",
Version: "5.2.2",
},
{
Name: "spring-boot",
Version: "2.2.2.RELEASE",
Version: "2.2.2",
},
{
Name: "spring-boot-starter-logging",
Version: "2.2.2.RELEASE",
Version: "2.2.2",
},
{
Name: "jakarta.annotation-api",
@@ -414,7 +383,7 @@ func TestParseNestedJar(t *testing.T) {
},
{
Name: "spring-webmvc",
Version: "5.2.2.RELEASE",
Version: "5.2.2",
},
{
Name: "HdrHistogram",
@@ -422,7 +391,7 @@ func TestParseNestedJar(t *testing.T) {
},
{
Name: "spring-boot-starter-web",
Version: "2.2.2.RELEASE",
Version: "2.2.2",
},
{
Name: "logback-classic",
@@ -434,7 +403,7 @@ func TestParseNestedJar(t *testing.T) {
},
{
Name: "spring-boot-starter-json",
Version: "2.2.2.RELEASE",
Version: "2.2.2",
},
{
Name: "jackson-databind",
@@ -450,7 +419,7 @@ func TestParseNestedJar(t *testing.T) {
},
{
Name: "spring-boot-autoconfigure",
Version: "2.2.2.RELEASE",
Version: "2.2.2",
},
{
Name: "jackson-datatype-jdk8",
@@ -466,11 +435,11 @@ func TestParseNestedJar(t *testing.T) {
},
{
Name: "spring-beans",
Version: "5.2.2.RELEASE",
Version: "5.2.2",
},
{
Name: "spring-boot-actuator",
Version: "2.2.2.RELEASE",
Version: "2.2.2",
},
{
Name: "slf4j-api",
@@ -478,7 +447,7 @@ func TestParseNestedJar(t *testing.T) {
},
{
Name: "spring-core",
Version: "5.2.2.RELEASE",
Version: "5.2.2",
},
{
Name: "logback-core",
@@ -498,7 +467,7 @@ func TestParseNestedJar(t *testing.T) {
},
{
Name: "spring-boot-starter-tomcat",
Version: "2.2.2.RELEASE",
Version: "2.2.2",
},
{
Name: "classmate",
@@ -506,7 +475,7 @@ func TestParseNestedJar(t *testing.T) {
},
{
Name: "spring-context",
Version: "5.2.2.RELEASE",
Version: "5.2.2",
},
},
},
@@ -527,7 +496,7 @@ func TestParseNestedJar(t *testing.T) {
t.Fatalf("failed to parse java archive: %+v", err)
}
nameVersionPairSet := internal.NewStringSet()
expectedNameVersionPairSet := internal.NewStringSet()
makeKey := func(p *pkg.Package) string {
if p == nil {
@@ -537,20 +506,32 @@ func TestParseNestedJar(t *testing.T) {
}
for _, e := range test.expected {
nameVersionPairSet.Add(makeKey(&e))
expectedNameVersionPairSet.Add(makeKey(&e))
}
if len(actual) != len(nameVersionPairSet) {
if len(actual) != len(expectedNameVersionPairSet) {
actualNameVersionPairSet := internal.NewStringSet()
for _, a := range actual {
t.Log(" ", a)
key := makeKey(&a)
actualNameVersionPairSet.Add(key)
if !expectedNameVersionPairSet.Contains(key) {
t.Logf("extra package: %s", a)
}
}
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(nameVersionPairSet))
for _, key := range expectedNameVersionPairSet.ToSlice() {
if !actualNameVersionPairSet.Contains(key) {
t.Logf("missing package: %s", key)
}
}
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(expectedNameVersionPairSet))
}
for _, a := range actual {
actualKey := makeKey(&a)
if !nameVersionPairSet.Contains(actualKey) {
if !expectedNameVersionPairSet.Contains(actualKey) {
t.Errorf("unexpected pkg: %q", actualKey)
}
@@ -563,7 +544,7 @@ func TestParseNestedJar(t *testing.T) {
if metadata.Parent == nil {
t.Errorf("unassigned error for pkg=%q", actualKey)
} else if makeKey(metadata.Parent) != "spring-boot|0.0.1-SNAPSHOT" {
// NB: this is a hard-coded condition to simplify the test harness
// NB: this is a hard-coded condition to simplify the test harness to account for https://github.com/micrometer-metrics/micrometer/issues/1785
if a.Name == "pcollections" {
if metadata.Parent.Name != "micrometer-core" {
t.Errorf("nested 'pcollections' pkg has wrong parent: %q", metadata.Parent.Name)
+3 -28
View File
@@ -4,40 +4,15 @@ Package java provides a concrete Cataloger implementation for Java archives (jar
package java
import (
"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.JavaPkg and pkg.JenkinsPluginPkg Package Types defined in java archive files.
type Cataloger struct {
cataloger common.GenericCataloger
}
// New returns a new Java archive cataloger object.
func New() *Cataloger {
// NewJavaCataloger returns a new Java archive cataloger object.
func NewJavaCataloger() *common.GenericCataloger {
globParsers := make(map[string]common.ParserFn)
for _, pattern := range archiveFormatGlobs {
globParsers[pattern] = parseJavaArchive
}
return &Cataloger{
cataloger: common.NewGenericCataloger(nil, globParsers),
}
}
// Name returns a string that uniquely describes this cataloger.
func (a *Cataloger) Name() string {
return "java-cataloger"
}
// SelectFiles returns a set of discovered Java archive files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver)
}
// Catalog returns the Packages indexed from all Java archive files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name())
return common.NewGenericCataloger(nil, globParsers, "java-cataloger")
}
+59 -36
View File
@@ -4,25 +4,41 @@ import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/pkg"
"github.com/mitchellh/mapstructure"
)
const manifestPath = "META-INF/MANIFEST.MF"
const manifestGlob = "/META-INF/MANIFEST.MF"
func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) {
// nolint:funlen
// parseJavaManifest takes MANIFEST.MF file content and returns sections of parsed key/value pairs.
// For more information: https://docs.oracle.com/en/java/javase/11/docs/specs/jar/jar.html#jar-manifest
func parseJavaManifest(path string, reader io.Reader) (*pkg.JavaManifest, error) {
var manifest pkg.JavaManifest
manifestMap := make(map[string]string)
sections := []map[string]string{
make(map[string]string),
}
currentSection := 0
scanner := bufio.NewScanner(reader)
var lastKey string
for scanner.Scan() {
line := scanner.Text()
// ignore empty lines
// empty lines denote section separators
if strings.TrimSpace(line) == "" {
currentSection++
// we don't want to allocate a new section map that wont necessarily be used, do that once there is
// a non-empty line to process
// do not process line continuations after this
lastKey = ""
continue
} else if currentSection >= len(sections) {
sections = append(sections, make(map[string]string))
}
if line[0] == ' ' {
@@ -30,7 +46,7 @@ func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) {
if lastKey == "" {
return nil, fmt.Errorf("found continuation with no previous key (%s)", line)
}
manifestMap[lastKey] += strings.TrimSpace(line)
sections[currentSection][lastKey] += strings.TrimSpace(line)
} else {
// this is a new key-value pair
idx := strings.Index(line, ":")
@@ -40,9 +56,9 @@ func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) {
key := strings.TrimSpace(line[0:idx])
value := strings.TrimSpace(line[idx+1:])
manifestMap[key] = value
sections[currentSection][key] = value
// keep track of key for future continuations
// keep track of key for potential future continuations
lastKey = key
}
}
@@ -51,14 +67,24 @@ func parseJavaManifest(reader io.Reader) (*pkg.JavaManifest, error) {
return nil, fmt.Errorf("unable to read java manifest: %w", err)
}
if err := mapstructure.Decode(manifestMap, &manifest); err != nil {
return nil, fmt.Errorf("unable to parse java manifest: %w", err)
}
// clean select fields
if strings.Trim(manifest.ImplVersion, " ") != "" {
// transform versions with dates attached to just versions (e.g. "1.3 2244 October 5 2008" --> "1.3")
manifest.ImplVersion = strings.Split(manifest.ImplVersion, " ")[0]
if len(sections) > 0 {
manifest.Main = sections[0]
if len(sections) > 1 {
manifest.NamedSections = make(map[string]map[string]string)
for i, s := range sections[1:] {
name, ok := s["Name"]
if !ok {
// per the manifest spec (https://docs.oracle.com/en/java/javase/11/docs/specs/jar/jar.html#jar-manifest)
// this should never happen. If it does we want to know about it, but not necessarily stop
// cataloging entirely... for this reason we only log.
log.Errorf("java manifest section found without a name: %s", path)
name = strconv.Itoa(i)
} else {
delete(s, "Name")
}
manifest.NamedSections[name] = s
}
}
}
return &manifest, nil
@@ -69,24 +95,21 @@ func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string
switch {
case filenameObj.name() != "":
name = filenameObj.name()
case manifest.Name != "":
case manifest.Main["Name"] != "":
// Manifest original spec...
name = manifest.Name
case manifest.Extra["Bundle-Name"] != "":
name = manifest.Main["Name"]
case manifest.Main["Bundle-Name"] != "":
// BND tooling...
name = manifest.Extra["Bundle-Name"]
case manifest.Extra["Short-Name"] != "":
name = manifest.Main["Bundle-Name"]
case manifest.Main["Short-Name"] != "":
// Jenkins...
name = manifest.Extra["Short-Name"]
case manifest.Extra["Extension-Name"] != "":
name = manifest.Main["Short-Name"]
case manifest.Main["Extension-Name"] != "":
// Jenkins...
name = manifest.Extra["Extension-Name"]
}
// in situations where we hit this point and no name was
// determined, look at the Implementation-Title
if name == "" && manifest.ImplTitle != "" {
name = manifest.ImplTitle
name = manifest.Main["Extension-Name"]
case manifest.Main["Implementation-Title"] != "":
// last ditch effort...
name = manifest.Main["Implementation-Title"]
}
return name
}
@@ -94,14 +117,14 @@ func selectName(manifest *pkg.JavaManifest, filenameObj archiveFilename) string
func selectVersion(manifest *pkg.JavaManifest, filenameObj archiveFilename) string {
var version string
switch {
case manifest.ImplVersion != "":
version = manifest.ImplVersion
case filenameObj.version() != "":
version = filenameObj.version()
case manifest.SpecVersion != "":
version = manifest.SpecVersion
case manifest.Extra["Plugin-Version"] != "":
version = manifest.Extra["Plugin-Version"]
case manifest.Main["Implementation-Version"] != "":
version = manifest.Main["Implementation-Version"]
case manifest.Main["Specification-Version"] != "":
version = manifest.Main["Specification-Version"]
case manifest.Main["Plugin-Version"] != "":
version = manifest.Main["Plugin-Version"]
}
return version
}
+78 -22
View File
@@ -2,10 +2,11 @@ package java
import (
"encoding/json"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
"os"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
func TestParseJavaManifest(t *testing.T) {
@@ -16,49 +17,62 @@ func TestParseJavaManifest(t *testing.T) {
{
fixture: "test-fixtures/manifest/small",
expected: pkg.JavaManifest{
ManifestVersion: "1.0",
Main: map[string]string{
"Manifest-Version": "1.0",
},
},
},
{
fixture: "test-fixtures/manifest/standard-info",
expected: pkg.JavaManifest{
ManifestVersion: "1.0",
Name: "the-best-name",
SpecTitle: "the-spec-title",
SpecVersion: "the-spec-version",
SpecVendor: "the-spec-vendor",
ImplTitle: "the-impl-title",
ImplVersion: "the-impl-version",
ImplVendor: "the-impl-vendor",
Main: map[string]string{
"Name": "the-best-name",
"Manifest-Version": "1.0",
"Specification-Title": "the-spec-title",
"Specification-Version": "the-spec-version",
"Specification-Vendor": "the-spec-vendor",
"Implementation-Title": "the-impl-title",
"Implementation-Version": "the-impl-version",
"Implementation-Vendor": "the-impl-vendor",
},
},
},
{
fixture: "test-fixtures/manifest/extra-info",
expected: pkg.JavaManifest{
ManifestVersion: "1.0",
Extra: map[string]string{
Main: map[string]string{
"Manifest-Version": "1.0",
"Archiver-Version": "Plexus Archiver",
"Build-Jdk": "14.0.1",
"Built-By": "?",
"Created-By": "Apache Maven 3.6.3",
"Main-Class": "hello.HelloWorld",
},
NamedSections: map[string]map[string]string{
"thing-1": {
"Built-By": "?",
},
"1": {
"Build-Jdk": "14.0.1",
"Main-Class": "hello.HelloWorld",
},
},
},
},
{
fixture: "test-fixtures/manifest/continuation",
expected: pkg.JavaManifest{
ManifestVersion: "1.0",
Extra: map[string]string{
"Plugin-ScmUrl": "https://github.com/jenkinsci/plugin-pom/example-jenkins-plugin",
Main: map[string]string{
"Manifest-Version": "1.0",
"Plugin-ScmUrl": "https://github.com/jenkinsci/plugin-pom/example-jenkins-plugin",
},
},
},
{
// regression test, we should always keep the full version
fixture: "test-fixtures/manifest/version-with-date",
expected: pkg.JavaManifest{
ManifestVersion: "1.0",
ImplVersion: "1.3", // ensure the date is stripped off during processing
Main: map[string]string{
"Manifest-Version": "1.0",
"Implementation-Version": "1.3 2244 October 5 2005",
},
},
},
}
@@ -70,7 +84,7 @@ func TestParseJavaManifest(t *testing.T) {
t.Fatalf("could not open fixture: %+v", err)
}
actual, err := parseJavaManifest(fixture)
actual, err := parseJavaManifest(test.fixture, fixture)
if err != nil {
t.Fatalf("failed to parse manifest: %+v", err)
}
@@ -91,3 +105,45 @@ func TestParseJavaManifest(t *testing.T) {
})
}
}
func TestSelectName(t *testing.T) {
tests := []struct {
desc string
manifest pkg.JavaManifest
archive archiveFilename
expected string
}{
{
desc: "Get name from Implementation-Title",
archive: archiveFilename{},
manifest: pkg.JavaManifest{
Main: map[string]string{
"Implementation-Title": "maven-wrapper",
},
},
expected: "maven-wrapper",
},
{
desc: "Implementation-Title does not override name from filename",
manifest: pkg.JavaManifest{
Main: map[string]string{
"Name": "foo",
"Implementation-Title": "maven-wrapper",
},
},
archive: newJavaArchiveFilename("/something/omg.jar"),
expected: "omg",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
result := selectName(&test.manifest, test.archive)
if result != test.expected {
t.Errorf("mismatch in names: '%s' != '%s'", result, test.expected)
}
})
}
}
+2 -1
View File
@@ -19,7 +19,7 @@ func saveArchiveToTmp(reader io.Reader) (string, string, func(), error) {
cleanupFn := func() {
err = os.RemoveAll(tempDir)
if err != nil {
log.Errorf("unable to cleanup jar tempdir: %w", err)
log.Errorf("unable to cleanup jar tempdir: %+v", err)
}
}
@@ -35,6 +35,7 @@ func saveArchiveToTmp(reader io.Reader) (string, string, func(), error) {
if err != nil {
return contentDir, "", cleanupFn, fmt.Errorf("unable to create archive: %w", err)
}
defer archiveFile.Close()
_, err = io.Copy(archiveFile, reader)
if err != nil {
@@ -1,4 +1,3 @@
<?jelly escape-by-default='true'?>
<div>
TODO
</div>
@@ -2,6 +2,7 @@ Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven 3.6.3
Name: thing-1
Built-By: ?
Build-Jdk: 14.0.1
+10 -26
View File
@@ -4,40 +4,24 @@ Package javascript provides a concrete Cataloger implementation for JavaScript e
package javascript
import (
"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.YarnPkg and pkg.NpmPkg Package Types defined in package-lock.json and yarn.lock files.
type Cataloger struct {
cataloger common.GenericCataloger
// NewJavascriptPackageCataloger returns a new JavaScript cataloger object based on detection of npm based packages.
func NewJavascriptPackageCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/package.json": parsePackageJSON,
}
return common.NewGenericCataloger(nil, globParsers, "javascript-package-cataloger")
}
// New returns a new JavaScript cataloger object.
func New() *Cataloger {
// NewJavascriptLockCataloger returns a new Javascript cataloger object base on package lock files.
func NewJavascriptLockCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/package-lock.json": parsePackageLock,
"**/yarn.lock": parseYarnLock,
}
return &Cataloger{
cataloger: common.NewGenericCataloger(nil, globParsers),
}
}
// Name returns a string that uniquely describes this cataloger.
func (a *Cataloger) Name() string {
return "javascript-cataloger"
}
// SelectFiles returns a set of discovered Javascript ecosystem files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver)
}
// Catalog returns the Packages indexed from all Javascript ecosystem files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name())
return common.NewGenericCataloger(nil, globParsers, "javascript-lock-cataloger")
}
@@ -0,0 +1,140 @@
package javascript
import (
"encoding/json"
"fmt"
"io"
"regexp"
"github.com/anchore/syft/internal"
"github.com/mitchellh/mapstructure"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg"
)
// integrity check
var _ common.ParserFn = parsePackageLock
// PackageJSON represents a JavaScript package.json file
type PackageJSON struct {
Version string `json:"version"`
Latest []string `json:"latest"`
Author Author `json:"author"`
License string `json:"license"`
Name string `json:"name"`
Homepage string `json:"homepage"`
Description string `json:"description"`
Dependencies map[string]string `json:"dependencies"`
Repository Repository `json:"repository"`
}
type Author struct {
Name string `json:"name" mapstruct:"name"`
Email string `json:"email" mapstruct:"email"`
URL string `json:"url" mapstruct:"url"`
}
type Repository struct {
Type string `json:"type" mapstructure:"type"`
URL string `json:"url" mapstructure:"url"`
}
// match example: "author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)"
// ---> name: "Isaac Z. Schlueter" email: "i@izs.me" url: "http://blog.izs.me"
var authorPattern = regexp.MustCompile(`^\s*(?P<name>[^<(]*)(\s+<(?P<email>.*)>)?(\s\((?P<url>.*)\))?\s*$`)
// Exports Author.UnmarshalJSON interface to help normalize the json structure.
func (a *Author) UnmarshalJSON(b []byte) error {
var authorStr string
var fields map[string]string
var author Author
if err := json.Unmarshal(b, &authorStr); err != nil {
// string parsing did not work, assume a map was given
// for more information: https://docs.npmjs.com/files/package.json#people-fields-author-contributors
if err := json.Unmarshal(b, &fields); err != nil {
return fmt.Errorf("unable to parse package.json author: %w", err)
}
} else {
// parse out "name <email> (url)" into an Author struct
fields = internal.MatchCaptureGroups(authorPattern, authorStr)
}
// translate the map into a structure
if err := mapstructure.Decode(fields, &author); err != nil {
return fmt.Errorf("unable to decode package.json author: %w", err)
}
*a = author
return nil
}
func (a *Author) AuthorString() string {
result := a.Name
if a.Email != "" {
result += fmt.Sprintf(" <%s>", a.Email)
}
if a.URL != "" {
result += fmt.Sprintf(" (%s)", a.URL)
}
return result
}
func (r *Repository) UnmarshalJSON(b []byte) error {
var repositoryStr string
var fields map[string]string
var repository Repository
if err := json.Unmarshal(b, &repositoryStr); err != nil {
// string parsing did not work, assume a map was given
// for more information: https://docs.npmjs.com/files/package.json#people-fields-author-contributors
if err := json.Unmarshal(b, &fields); err != nil {
return fmt.Errorf("unable to parse package.json author: %w", err)
}
// translate the map into a structure
if err := mapstructure.Decode(fields, &repository); err != nil {
return fmt.Errorf("unable to decode package.json author: %w", err)
}
*r = repository
} else {
r.URL = repositoryStr
}
return nil
}
// parsePackageJson parses a package.json and returns the discovered JavaScript packages.
func parsePackageJSON(_ string, reader io.Reader) ([]pkg.Package, error) {
packages := make([]pkg.Package, 0)
dec := json.NewDecoder(reader)
for {
var p PackageJSON
if err := dec.Decode(&p); err == io.EOF {
break
} else if err != nil {
return nil, fmt.Errorf("failed to parse package.json file: %w", err)
}
packages = append(packages, pkg.Package{
Name: p.Name,
Version: p.Version,
Licenses: []string{p.License},
Language: pkg.JavaScript,
Type: pkg.NpmPkg,
MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{
Author: p.Author.AuthorString(),
Homepage: p.Homepage,
URL: p.Repository.URL,
Licenses: []string{p.License},
},
})
}
return packages, nil
}
@@ -0,0 +1,93 @@
package javascript
import (
"os"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
func TestParsePackageJSON(t *testing.T) {
tests := []struct {
Fixture string
ExpectedPkg pkg.Package
}{
{
Fixture: "test-fixtures/pkg-json/package.json",
ExpectedPkg: pkg.Package{
Name: "npm",
Version: "6.14.6",
Type: pkg.NpmPkg,
Licenses: []string{"Artistic-2.0"},
Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Licenses: []string{"Artistic-2.0"},
},
},
},
{
Fixture: "test-fixtures/pkg-json/package-nested-author.json",
ExpectedPkg: pkg.Package{
Name: "npm",
Version: "6.14.6",
Type: pkg.NpmPkg,
Licenses: []string{"Artistic-2.0"},
Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{
Author: "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
Homepage: "https://docs.npmjs.com/",
URL: "https://github.com/npm/cli",
Licenses: []string{"Artistic-2.0"},
},
},
},
{
Fixture: "test-fixtures/pkg-json/package-repo-string.json",
ExpectedPkg: pkg.Package{
Name: "function-bind",
Version: "1.1.1",
Type: pkg.NpmPkg,
Licenses: []string{"MIT"},
Language: pkg.JavaScript,
MetadataType: pkg.NpmPackageJSONMetadataType,
Metadata: pkg.NpmPackageJSONMetadata{
Author: "Raynos <raynos2@gmail.com>",
Homepage: "https://github.com/Raynos/function-bind",
URL: "git://github.com/Raynos/function-bind.git",
Licenses: []string{"MIT"},
},
},
},
}
for _, test := range tests {
t.Run(test.Fixture, func(t *testing.T) {
fixture, err := os.Open(test.Fixture)
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parsePackageJSON(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse package-lock.json: %+v", err)
}
if len(actual) != 1 {
for _, a := range actual {
t.Log(" ", a)
}
t.Fatalf("unexpected package count: %d!=1", len(actual))
}
for _, d := range deep.Equal(actual[0], test.ExpectedPkg) {
t.Errorf("diff: %+v", d)
}
})
}
}
+1 -1
View File
@@ -66,7 +66,7 @@ func parseYarnLock(_ string, reader io.Reader) ([]pkg.Package, error) {
Name: currentName,
Version: strings.Trim(version, "\""),
Language: pkg.JavaScript,
Type: pkg.YarnPkg,
Type: pkg.NpmPkg,
})
}
}
@@ -13,43 +13,43 @@ func TestParseYarnLock(t *testing.T) {
Name: "@babel/code-frame",
Version: "7.10.4",
Language: pkg.JavaScript,
Type: pkg.YarnPkg,
Type: pkg.NpmPkg,
},
"@types/minimatch": {
Name: "@types/minimatch",
Version: "3.0.3",
Language: pkg.JavaScript,
Type: pkg.YarnPkg,
Type: pkg.NpmPkg,
},
"@types/qs": {
Name: "@types/qs",
Version: "6.9.4",
Language: pkg.JavaScript,
Type: pkg.YarnPkg,
Type: pkg.NpmPkg,
},
"ajv": {
Name: "ajv",
Version: "6.12.3",
Language: pkg.JavaScript,
Type: pkg.YarnPkg,
Type: pkg.NpmPkg,
},
"atob": {
Name: "atob",
Version: "2.1.2",
Language: pkg.JavaScript,
Type: pkg.YarnPkg,
Type: pkg.NpmPkg,
},
"aws-sdk": {
Name: "aws-sdk",
Version: "2.706.0",
Language: pkg.JavaScript,
Type: pkg.YarnPkg,
Type: pkg.NpmPkg,
},
"jhipster-core": {
Name: "jhipster-core",
Version: "7.3.4",
Language: pkg.JavaScript,
Type: pkg.YarnPkg,
Type: pkg.NpmPkg,
},
}
fixture, err := os.Open("test-fixtures/yarn/yarn.lock")
@@ -0,0 +1,16 @@
{
"version": "6.14.6",
"name": "npm",
"description": "a package manager for JavaScript",
"homepage": "https://docs.npmjs.com/",
"author": {
"name": "Isaac Z. Schlueter",
"email": "i@izs.me",
"url": "http://blog.izs.me"
},
"repository": {
"type": "git",
"url": "https://github.com/npm/cli"
},
"license": "Artistic-2.0"
}
@@ -0,0 +1,66 @@
{
"name": "function-bind",
"version": "1.1.1",
"description": "Implementation of Function.prototype.bind",
"keywords": [
"function",
"bind",
"shim",
"es5"
],
"author": "Raynos <raynos2@gmail.com>",
"repository": "git://github.com/Raynos/function-bind.git",
"main": "index",
"homepage": "https://github.com/Raynos/function-bind",
"contributors": [
{
"name": "Raynos"
},
{
"name": "Jordan Harband",
"url": "https://github.com/ljharb"
}
],
"bugs": {
"url": "https://github.com/Raynos/function-bind/issues",
"email": "raynos2@gmail.com"
},
"dependencies": {},
"devDependencies": {
"@ljharb/eslint-config": "^12.2.1",
"covert": "^1.1.0",
"eslint": "^4.5.0",
"jscs": "^3.0.7",
"tape": "^4.8.0"
},
"license": "MIT",
"scripts": {
"pretest": "npm run lint",
"test": "npm run tests-only",
"posttest": "npm run coverage -- --quiet",
"tests-only": "node test",
"coverage": "covert test/*.js",
"lint": "npm run jscs && npm run eslint",
"jscs": "jscs *.js */*.js",
"eslint": "eslint *.js */*.js"
},
"testling": {
"files": "test/index.js",
"browsers": [
"ie/8..latest",
"firefox/16..latest",
"firefox/nightly",
"chrome/22..latest",
"chrome/canary",
"opera/12..latest",
"opera/next",
"safari/5.1..latest",
"ipad/6.0..latest",
"iphone/6.0..latest",
"android-browser/4.2..latest"
]
}
,"_resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz"
,"_integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
,"_from": "function-bind@1.1.1"
}
@@ -0,0 +1,314 @@
{
"version": "6.14.6",
"name": "npm",
"description": "a package manager for JavaScript",
"keywords": [
"install",
"modules",
"package manager",
"package.json"
],
"preferGlobal": true,
"config": {
"publishtest": false
},
"homepage": "https://docs.npmjs.com/",
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me)",
"repository": {
"type": "git",
"url": "https://github.com/npm/cli"
},
"bugs": {
"url": "https://npm.community/c/bugs"
},
"directories": {
"bin": "./bin",
"doc": "./doc",
"lib": "./lib",
"man": "./man"
},
"main": "./lib/npm.js",
"bin": {
"npm": "./bin/npm-cli.js",
"npx": "./bin/npx-cli.js"
},
"dependencies": {
"JSONStream": "^1.3.5",
"abbrev": "~1.1.1",
"ansicolors": "~0.3.2",
"ansistyles": "~0.1.3",
"aproba": "^2.0.0",
"archy": "~1.0.0",
"bin-links": "^1.1.7",
"bluebird": "^3.5.5",
"byte-size": "^5.0.1",
"cacache": "^12.0.3",
"call-limit": "^1.1.1",
"chownr": "^1.1.4",
"ci-info": "^2.0.0",
"cli-columns": "^3.1.2",
"cli-table3": "^0.5.1",
"cmd-shim": "^3.0.3",
"columnify": "~1.5.4",
"config-chain": "^1.1.12",
"detect-indent": "~5.0.0",
"detect-newline": "^2.1.0",
"dezalgo": "~1.0.3",
"editor": "~1.0.0",
"figgy-pudding": "^3.5.1",
"find-npm-prefix": "^1.0.2",
"fs-vacuum": "~1.2.10",
"fs-write-stream-atomic": "~1.0.10",
"gentle-fs": "^2.3.0",
"glob": "^7.1.6",
"graceful-fs": "^4.2.4",
"has-unicode": "~2.0.1",
"hosted-git-info": "^2.8.8",
"iferr": "^1.0.2",
"infer-owner": "^1.0.4",
"inflight": "~1.0.6",
"inherits": "^2.0.4",
"ini": "^1.3.5",
"init-package-json": "^1.10.3",
"is-cidr": "^3.0.0",
"json-parse-better-errors": "^1.0.2",
"lazy-property": "~1.0.0",
"libcipm": "^4.0.7",
"libnpm": "^3.0.1",
"libnpmaccess": "^3.0.2",
"libnpmhook": "^5.0.3",
"libnpmorg": "^1.0.1",
"libnpmsearch": "^2.0.2",
"libnpmteam": "^1.0.2",
"libnpx": "^10.2.2",
"lock-verify": "^2.1.0",
"lockfile": "^1.0.4",
"lodash._baseuniq": "~4.6.0",
"lodash.clonedeep": "~4.5.0",
"lodash.union": "~4.6.0",
"lodash.uniq": "~4.5.0",
"lodash.without": "~4.4.0",
"lru-cache": "^5.1.1",
"meant": "~1.0.1",
"mississippi": "^3.0.0",
"mkdirp": "^0.5.5",
"move-concurrently": "^1.0.1",
"node-gyp": "^5.1.0",
"nopt": "^4.0.3",
"normalize-package-data": "^2.5.0",
"npm-audit-report": "^1.3.2",
"npm-cache-filename": "~1.0.2",
"npm-install-checks": "^3.0.2",
"npm-lifecycle": "^3.1.4",
"npm-package-arg": "^6.1.1",
"npm-packlist": "^1.4.8",
"npm-pick-manifest": "^3.0.2",
"npm-profile": "^4.0.4",
"npm-registry-fetch": "^4.0.5",
"npm-user-validate": "~1.0.0",
"npmlog": "~4.1.2",
"once": "~1.4.0",
"opener": "^1.5.1",
"osenv": "^0.1.5",
"pacote": "^9.5.12",
"path-is-inside": "~1.0.2",
"promise-inflight": "~1.0.1",
"qrcode-terminal": "^0.12.0",
"query-string": "^6.8.2",
"qw": "~1.0.1",
"read": "~1.0.7",
"read-cmd-shim": "^1.0.5",
"read-installed": "~4.0.3",
"read-package-json": "^2.1.1",
"read-package-tree": "^5.3.1",
"readable-stream": "^3.6.0",
"readdir-scoped-modules": "^1.1.0",
"request": "^2.88.0",
"retry": "^0.12.0",
"rimraf": "^2.7.1",
"safe-buffer": "^5.1.2",
"semver": "^5.7.1",
"sha": "^3.0.0",
"slide": "~1.1.6",
"sorted-object": "~2.0.1",
"sorted-union-stream": "~2.1.3",
"ssri": "^6.0.1",
"stringify-package": "^1.0.1",
"tar": "^4.4.13",
"text-table": "~0.2.0",
"tiny-relative-date": "^1.3.0",
"uid-number": "0.0.6",
"umask": "~1.1.0",
"unique-filename": "^1.1.1",
"unpipe": "~1.0.0",
"update-notifier": "^2.5.0",
"uuid": "^3.3.3",
"validate-npm-package-license": "^3.0.4",
"validate-npm-package-name": "~3.0.0",
"which": "^1.3.1",
"worker-farm": "^1.7.0",
"write-file-atomic": "^2.4.3"
},
"bundleDependencies": [
"abbrev",
"ansicolors",
"ansistyles",
"aproba",
"archy",
"bin-links",
"bluebird",
"byte-size",
"cacache",
"call-limit",
"chownr",
"ci-info",
"cli-columns",
"cli-table3",
"cmd-shim",
"columnify",
"config-chain",
"debuglog",
"detect-indent",
"detect-newline",
"dezalgo",
"editor",
"figgy-pudding",
"find-npm-prefix",
"fs-vacuum",
"fs-write-stream-atomic",
"gentle-fs",
"glob",
"graceful-fs",
"has-unicode",
"hosted-git-info",
"iferr",
"imurmurhash",
"infer-owner",
"inflight",
"inherits",
"ini",
"init-package-json",
"is-cidr",
"json-parse-better-errors",
"JSONStream",
"lazy-property",
"libcipm",
"libnpm",
"libnpmaccess",
"libnpmhook",
"libnpmorg",
"libnpmsearch",
"libnpmteam",
"libnpx",
"lock-verify",
"lockfile",
"lodash._baseindexof",
"lodash._baseuniq",
"lodash._bindcallback",
"lodash._cacheindexof",
"lodash._createcache",
"lodash._getnative",
"lodash.clonedeep",
"lodash.restparam",
"lodash.union",
"lodash.uniq",
"lodash.without",
"lru-cache",
"meant",
"mississippi",
"mkdirp",
"move-concurrently",
"node-gyp",
"nopt",
"normalize-package-data",
"npm-audit-report",
"npm-cache-filename",
"npm-install-checks",
"npm-lifecycle",
"npm-package-arg",
"npm-packlist",
"npm-pick-manifest",
"npm-profile",
"npm-registry-fetch",
"npm-user-validate",
"npmlog",
"once",
"opener",
"osenv",
"pacote",
"path-is-inside",
"promise-inflight",
"qrcode-terminal",
"query-string",
"qw",
"read-cmd-shim",
"read-installed",
"read-package-json",
"read-package-tree",
"read",
"readable-stream",
"readdir-scoped-modules",
"request",
"retry",
"rimraf",
"safe-buffer",
"semver",
"sha",
"slide",
"sorted-object",
"sorted-union-stream",
"ssri",
"stringify-package",
"tar",
"text-table",
"tiny-relative-date",
"uid-number",
"umask",
"unique-filename",
"unpipe",
"update-notifier",
"uuid",
"validate-npm-package-license",
"validate-npm-package-name",
"which",
"worker-farm",
"write-file-atomic"
],
"devDependencies": {
"deep-equal": "^1.0.1",
"get-stream": "^4.1.0",
"licensee": "^7.0.3",
"marked": "^0.6.3",
"marked-man": "^0.6.0",
"npm-registry-couchapp": "^2.7.4",
"npm-registry-mock": "^1.3.1",
"require-inject": "^1.4.4",
"sprintf-js": "^1.1.2",
"standard": "^11.0.1",
"tacks": "^1.3.0",
"tap": "^12.7.0",
"tar-stream": "^2.1.0"
},
"scripts": {
"dumpconf": "env | grep npm | sort | uniq",
"prepare": "node bin/npm-cli.js rebuild && node bin/npm-cli.js --no-audit --no-timing prune --prefix=. --no-global && rimraf test/*/*/node_modules && make -j4 mandocs",
"preversion": "bash scripts/update-authors.sh && git add AUTHORS && git commit -m \"update AUTHORS\" || true",
"licenses": "licensee --production --errors-only",
"tap": "tap -J --timeout 300 --no-esm",
"tap-cover": "tap -J --nyc-arg=--cache --coverage --timeout 600 --no-esm",
"lint": "standard",
"pretest": "npm run lint",
"test": "npm run test-tap --",
"test:nocleanup": "NO_TEST_CLEANUP=1 npm run test --",
"sudotest": "sudo npm run tap -- \"test/tap/*.js\"",
"sudotest:nocleanup": "sudo NO_TEST_CLEANUP=1 npm run tap -- \"test/tap/*.js\"",
"posttest": "rimraf test/npm_cache*",
"test-coverage": "npm run tap-cover -- \"test/tap/*.js\" \"test/network/*.js\"",
"test-tap": "npm run tap -- \"test/tap/*.js\" \"test/network/*.js\"",
"test-node": "tap --timeout 240 \"test/tap/*.js\" \"test/network/*.js\""
},
"license": "Artistic-2.0",
"engines": {
"node": "6 >=6.2.0 || 8 || >=9.3.0"
}
}
-46
View File
@@ -1,46 +0,0 @@
/*
Package python provides a concrete Cataloger implementation for Python ecosystem files (egg, wheel, requirements.txt).
*/
package python
import (
"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.WheelPkg, pkg.EggPkg, and pkg.PythonRequirementsPkg Package Types defined in Python ecosystem files.
type Cataloger struct {
cataloger common.GenericCataloger
}
// New returns a new Python cataloger object.
func New() *Cataloger {
globParsers := map[string]common.ParserFn{
"**/*egg-info/PKG-INFO": parseEggMetadata,
"**/*dist-info/METADATA": parseWheelMetadata,
"**/requirements.txt": parseRequirementsTxt,
"**/poetry.lock": parsePoetryLock,
"**/setup.py": parseSetup,
}
return &Cataloger{
cataloger: common.NewGenericCataloger(nil, globParsers),
}
}
// Name returns a string that uniquely describes this cataloger.
func (a *Cataloger) Name() string {
return "python-cataloger"
}
// SelectFiles returns a set of discovered Python ecosystem files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver)
}
// Catalog returns the Packages indexed from all Python ecosystem files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name())
}
+19
View File
@@ -0,0 +1,19 @@
/*
Package python provides a concrete Cataloger implementation for Python ecosystem files (egg, wheel, requirements.txt).
*/
package python
import (
"github.com/anchore/syft/syft/cataloger/common"
)
// NewPythonIndexCataloger returns a new cataloger for python packages referenced from poetry lock files, requirements.txt files, and setup.py files.
func NewPythonIndexCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/*requirements*.txt": parseRequirementsTxt,
"**/poetry.lock": parsePoetryLock,
"**/setup.py": parseSetup,
}
return common.NewGenericCataloger(nil, globParsers, "python-index-cataloger")
}
+179
View File
@@ -0,0 +1,179 @@
package python
import (
"bufio"
"fmt"
"path/filepath"
"strings"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/scope"
)
const (
eggMetadataGlob = "**/*egg-info/PKG-INFO"
wheelMetadataGlob = "**/*dist-info/METADATA"
)
type PackageCataloger struct{}
// NewPythonPackageCataloger returns a new cataloger for python packages within egg or wheel installation directories.
func NewPythonPackageCataloger() *PackageCataloger {
return &PackageCataloger{}
}
// Name returns a string that uniquely describes a cataloger
func (c *PackageCataloger) Name() string {
return "python-package-cataloger"
}
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing python egg and wheel installations.
func (c *PackageCataloger) Catalog(resolver scope.Resolver) ([]pkg.Package, error) {
// nolint:prealloc
var fileMatches []file.Reference
for _, glob := range []string{eggMetadataGlob, wheelMetadataGlob} {
matches, err := resolver.FilesByGlob(glob)
if err != nil {
return nil, fmt.Errorf("failed to find files by glob: %s", glob)
}
fileMatches = append(fileMatches, matches...)
}
var pkgs []pkg.Package
for _, ref := range fileMatches {
p, err := c.catalogEggOrWheel(resolver, ref)
if err != nil {
return nil, fmt.Errorf("unable to catalog python package=%+v: %w", ref.Path, err)
}
if p != nil {
pkgs = append(pkgs, *p)
}
}
return pkgs, nil
}
// catalogEggOrWheel takes the primary metadata file reference and returns the python package it represents.
func (c *PackageCataloger) catalogEggOrWheel(resolver scope.Resolver, metadataRef file.Reference) (*pkg.Package, error) {
metadata, sources, err := c.assembleEggOrWheelMetadata(resolver, metadataRef)
if err != nil {
return nil, err
}
var licenses []string
if metadata.License != "" {
licenses = []string{metadata.License}
}
return &pkg.Package{
Name: metadata.Name,
Version: metadata.Version,
FoundBy: c.Name(),
Source: sources,
Licenses: licenses,
Language: pkg.Python,
Type: pkg.PythonPkg,
MetadataType: pkg.PythonPackageMetadataType,
Metadata: *metadata,
}, nil
}
// fetchRecordFiles finds a corresponding RECORD file for the given python package metadata file and returns the set of file records contained.
func (c *PackageCataloger) fetchRecordFiles(resolver scope.Resolver, metadataRef file.Reference) (files []pkg.PythonFileRecord, sources []file.Reference, err error) {
// we've been given a file reference to a specific wheel METADATA file. note: this may be for a directory
// or for an image... for an image the METADATA file may be present within multiple layers, so it is important
// to reconcile the RECORD path to the same layer (or the next adjacent lower layer).
// lets find the RECORD file relative to the directory where the METADATA file resides (in path AND layer structure)
recordPath := filepath.Join(filepath.Dir(string(metadataRef.Path)), "RECORD")
recordRef, err := resolver.RelativeFileByPath(metadataRef, recordPath)
if err != nil {
return nil, nil, err
}
if recordRef != nil {
sources = append(sources, *recordRef)
recordContents, err := resolver.FileContentsByRef(*recordRef)
if err != nil {
return nil, nil, err
}
// parse the record contents
records, err := parseWheelOrEggRecord(strings.NewReader(recordContents))
if err != nil {
return nil, nil, err
}
files = append(files, records...)
}
return files, sources, nil
}
// fetchTopLevelPackages finds a corresponding top_level.txt file for the given python package metadata file and returns the set of package names contained.
func (c *PackageCataloger) fetchTopLevelPackages(resolver scope.Resolver, metadataRef file.Reference) (pkgs []string, sources []file.Reference, err error) {
// a top_level.txt file specifies the python top-level packages (provided by this python package) installed into site-packages
parentDir := filepath.Dir(string(metadataRef.Path))
topLevelPath := filepath.Join(parentDir, "top_level.txt")
topLevelRef, err := resolver.RelativeFileByPath(metadataRef, topLevelPath)
if err != nil {
return nil, nil, err
}
if topLevelRef == nil {
return nil, nil, fmt.Errorf("missing python package top_level.txt (package=%q)", string(metadataRef.Path))
}
sources = append(sources, *topLevelRef)
topLevelContents, err := resolver.FileContentsByRef(*topLevelRef)
if err != nil {
return nil, nil, err
}
scanner := bufio.NewScanner(strings.NewReader(topLevelContents))
for scanner.Scan() {
pkgs = append(pkgs, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, nil, fmt.Errorf("could not read python package top_level.txt: %w", err)
}
return pkgs, sources, nil
}
// assembleEggOrWheelMetadata discovers and accumulates python package metadata from multiple file sources and returns a single metadata object as well as a list of files where the metadata was derived from.
func (c *PackageCataloger) assembleEggOrWheelMetadata(resolver scope.Resolver, metadataRef file.Reference) (*pkg.PythonPackageMetadata, []file.Reference, error) {
var sources = []file.Reference{metadataRef}
metadataContents, err := resolver.FileContentsByRef(metadataRef)
if err != nil {
return nil, nil, err
}
metadata, err := parseWheelOrEggMetadata(metadataRef.Path, strings.NewReader(metadataContents))
if err != nil {
return nil, nil, err
}
// attach any python files found for the given wheel/egg installation
r, s, err := c.fetchRecordFiles(resolver, metadataRef)
if err != nil {
return nil, nil, err
}
sources = append(sources, s...)
metadata.Files = r
// attach any top-level package names found for the given wheel/egg installation
p, s, err := c.fetchTopLevelPackages(resolver, metadataRef)
if err != nil {
return nil, nil, err
}
sources = append(sources, s...)
metadata.TopLevelPackages = p
return &metadata, sources, nil
}
@@ -0,0 +1,241 @@
package python
import (
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"testing"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
type pythonTestResolverMock struct {
metadataReader io.Reader
recordReader io.Reader
topLevelReader io.Reader
metadataRef *file.Reference
recordRef *file.Reference
topLevelRef *file.Reference
contents map[file.Reference]string
}
func newTestResolver(metaPath, recordPath, topPath string) *pythonTestResolverMock {
metadataReader, err := os.Open(metaPath)
if err != nil {
panic(fmt.Errorf("failed to open metadata: %+v", err))
}
var recordReader io.Reader
if recordPath != "" {
recordReader, err = os.Open(recordPath)
if err != nil {
panic(fmt.Errorf("failed to open record: %+v", err))
}
}
var topLevelReader io.Reader
if topPath != "" {
topLevelReader, err = os.Open(topPath)
if err != nil {
panic(fmt.Errorf("failed to open top level: %+v", err))
}
}
var recordRef *file.Reference
if recordReader != nil {
ref := file.NewFileReference("test-fixtures/dist-info/RECORD")
recordRef = &ref
}
var topLevelRef *file.Reference
if topLevelReader != nil {
ref := file.NewFileReference("test-fixtures/dist-info/top_level.txt")
topLevelRef = &ref
}
metadataRef := file.NewFileReference("test-fixtures/dist-info/METADATA")
return &pythonTestResolverMock{
recordReader: recordReader,
metadataReader: metadataReader,
topLevelReader: topLevelReader,
metadataRef: &metadataRef,
recordRef: recordRef,
topLevelRef: topLevelRef,
contents: make(map[file.Reference]string),
}
}
func (r *pythonTestResolverMock) FileContentsByRef(ref file.Reference) (string, error) {
switch ref.Path {
case r.topLevelRef.Path:
b, err := ioutil.ReadAll(r.topLevelReader)
if err != nil {
return "", err
}
return string(b), nil
case r.metadataRef.Path:
b, err := ioutil.ReadAll(r.metadataReader)
if err != nil {
return "", err
}
return string(b), nil
case r.recordRef.Path:
b, err := ioutil.ReadAll(r.recordReader)
if err != nil {
return "", err
}
return string(b), nil
}
return "", fmt.Errorf("invalid value given")
}
func (r *pythonTestResolverMock) MultipleFileContentsByRef(_ ...file.Reference) (map[file.Reference]string, error) {
return nil, fmt.Errorf("not implemented")
}
func (r *pythonTestResolverMock) FilesByPath(_ ...file.Path) ([]file.Reference, error) {
return nil, fmt.Errorf("not implemented")
}
func (r *pythonTestResolverMock) FilesByGlob(_ ...string) ([]file.Reference, error) {
return nil, fmt.Errorf("not implemented")
}
func (r *pythonTestResolverMock) RelativeFileByPath(_ file.Reference, path string) (*file.Reference, error) {
switch {
case strings.Contains(path, "RECORD"):
return r.recordRef, nil
case strings.Contains(path, "top_level.txt"):
return r.topLevelRef, nil
default:
return nil, fmt.Errorf("invalid RelativeFileByPath value given: %q", path)
}
}
func TestPythonPackageWheelCataloger(t *testing.T) {
tests := []struct {
MetadataFixture string
RecordFixture string
TopLevelFixture string
ExpectedPackage pkg.Package
}{
{
MetadataFixture: "test-fixtures/egg-info/PKG-INFO",
RecordFixture: "test-fixtures/egg-info/RECORD",
TopLevelFixture: "test-fixtures/egg-info/top_level.txt",
ExpectedPackage: pkg.Package{
Name: "requests",
Version: "2.22.0",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: []string{"Apache 2.0"},
FoundBy: "python-package-cataloger",
MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{
Name: "requests",
Version: "2.22.0",
License: "Apache 2.0",
Platform: "UNKNOWN",
Author: "Kenneth Reitz",
AuthorEmail: "me@kennethreitz.org",
SitePackagesRootPath: "test-fixtures",
Files: []pkg.PythonFileRecord{
{Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.Digest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"},
{Path: "requests/__init__.py", Digest: &pkg.Digest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"},
{Path: "requests/__pycache__/__version__.cpython-38.pyc"},
{Path: "requests/__pycache__/utils.cpython-38.pyc"},
{Path: "requests/__version__.py", Digest: &pkg.Digest{"sha256", "Bm-GFstQaFezsFlnmEMrJDe8JNROz9n2XXYtODdvjjc"}, Size: "436"},
{Path: "requests/utils.py", Digest: &pkg.Digest{"sha256", "LtPJ1db6mJff2TJSJWKi7rBpzjPS3mSOrjC9zRhoD3A"}, Size: "30049"},
},
TopLevelPackages: []string{"requests"},
},
},
},
{
MetadataFixture: "test-fixtures/dist-info/METADATA",
RecordFixture: "test-fixtures/dist-info/RECORD",
TopLevelFixture: "test-fixtures/dist-info/top_level.txt",
ExpectedPackage: pkg.Package{
Name: "Pygments",
Version: "2.6.1",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: []string{"BSD License"},
FoundBy: "python-package-cataloger",
MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{
Name: "Pygments",
Version: "2.6.1",
License: "BSD License",
Platform: "any",
Author: "Georg Brandl",
AuthorEmail: "georg@python.org",
SitePackagesRootPath: "test-fixtures",
Files: []pkg.PythonFileRecord{
{Path: "../../../bin/pygmentize", Digest: &pkg.Digest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"},
{Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.Digest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"},
{Path: "Pygments-2.6.1.dist-info/RECORD"},
{Path: "pygments/__pycache__/__init__.cpython-38.pyc"},
{Path: "pygments/util.py", Digest: &pkg.Digest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},
},
TopLevelPackages: []string{"pygments", "something_else"},
},
},
},
{
// in cases where the metadata file is available and the record is not we should still record there is a package
// additionally empty top_level.txt files should not result in an error
MetadataFixture: "test-fixtures/partial.dist-info/METADATA",
TopLevelFixture: "test-fixtures/partial.dist-info/top_level.txt",
ExpectedPackage: pkg.Package{
Name: "Pygments",
Version: "2.6.1",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: []string{"BSD License"},
FoundBy: "python-package-cataloger",
MetadataType: pkg.PythonPackageMetadataType,
Metadata: pkg.PythonPackageMetadata{
Name: "Pygments",
Version: "2.6.1",
License: "BSD License",
Platform: "any",
Author: "Georg Brandl",
AuthorEmail: "georg@python.org",
SitePackagesRootPath: "test-fixtures",
},
},
},
}
for _, test := range tests {
t.Run(test.MetadataFixture, func(t *testing.T) {
resolver := newTestResolver(test.MetadataFixture, test.RecordFixture, test.TopLevelFixture)
// note that the source is the record ref created by the resolver mock... attach the expected values
test.ExpectedPackage.Source = []file.Reference{*resolver.metadataRef}
if resolver.recordRef != nil {
test.ExpectedPackage.Source = append(test.ExpectedPackage.Source, *resolver.recordRef)
}
if resolver.topLevelRef != nil {
test.ExpectedPackage.Source = append(test.ExpectedPackage.Source, *resolver.topLevelRef)
}
// end patching expected values with runtime data...
pyPkgCataloger := NewPythonPackageCataloger()
actual, err := pyPkgCataloger.catalogEggOrWheel(resolver, *resolver.metadataRef)
if err != nil {
t.Fatalf("failed to catalog python package: %+v", err)
}
for _, d := range deep.Equal(actual, &test.ExpectedPackage) {
t.Errorf("diff: %+v", d)
}
})
}
}
@@ -1,10 +1,11 @@
package python
import (
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
"os"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
func TestParsePoetryLock(t *testing.T) {
@@ -13,28 +14,28 @@ func TestParsePoetryLock(t *testing.T) {
Name: "added-value",
Version: "0.14.2",
Language: pkg.Python,
Type: pkg.PoetryPkg,
Type: pkg.PythonPkg,
Licenses: nil,
},
{
Name: "alabaster",
Version: "0.7.12",
Language: pkg.Python,
Type: pkg.PoetryPkg,
Type: pkg.PythonPkg,
Licenses: nil,
},
{
Name: "appnope",
Version: "0.1.0",
Language: pkg.Python,
Type: pkg.PoetryPkg,
Type: pkg.PythonPkg,
Licenses: nil,
},
{
Name: "asciitree",
Version: "0.3.3",
Language: pkg.Python,
Type: pkg.PoetryPkg,
Type: pkg.PythonPkg,
Licenses: nil,
},
}
+1 -1
View File
@@ -47,7 +47,7 @@ func parseRequirementsTxt(_ string, reader io.Reader) ([]pkg.Package, error) {
Name: name,
Version: version,
Language: pkg.Python,
Type: pkg.PythonRequirementsPkg,
Type: pkg.PythonPkg,
})
default:
continue
@@ -4,24 +4,45 @@ import (
"os"
"testing"
"github.com/go-test/deep"
"github.com/anchore/syft/syft/pkg"
)
func assertPackagesEqual(t *testing.T, actual []pkg.Package, expected map[string]pkg.Package) {
t.Helper()
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 {
expectedPkg, ok := expected[a.Name]
if !ok {
t.Errorf("unexpected package found: '%s'", a.Name)
}
for _, d := range deep.Equal(a, expectedPkg) {
t.Errorf("diff: %+v", d)
}
}
}
func TestParseRequirementsTxt(t *testing.T) {
expected := map[string]pkg.Package{
"foo": {
Name: "foo",
Version: "1.0.0",
Language: pkg.Python,
Type: pkg.PythonRequirementsPkg,
Licenses: []string{},
Type: pkg.PythonPkg,
},
"flask": {
Name: "flask",
Version: "4.0.0",
Language: pkg.Python,
Type: pkg.PythonRequirementsPkg,
Licenses: []string{},
Type: pkg.PythonPkg,
},
}
fixture, err := os.Open("test-fixtures/requires/requirements.txt")
@@ -34,6 +55,6 @@ func TestParseRequirementsTxt(t *testing.T) {
t.Fatalf("failed to parse requirements: %+v", err)
}
assertPkgsEqual(t, actual, expected)
assertPackagesEqual(t, actual, expected)
}
+1 -1
View File
@@ -41,7 +41,7 @@ func parseSetup(_ string, reader io.Reader) ([]pkg.Package, error) {
Name: strings.Trim(name, "'\""),
Version: strings.Trim(version, "'\""),
Language: pkg.Python,
Type: pkg.PythonSetupPkg,
Type: pkg.PythonPkg,
})
}
}
+6 -11
View File
@@ -13,36 +13,31 @@ func TestParseSetup(t *testing.T) {
Name: "pathlib3",
Version: "2.2.0",
Language: pkg.Python,
Type: pkg.PythonSetupPkg,
Licenses: []string{},
Type: pkg.PythonPkg,
},
"mypy": {
Name: "mypy",
Version: "v0.770",
Language: pkg.Python,
Type: pkg.PythonSetupPkg,
Licenses: []string{},
Type: pkg.PythonPkg,
},
"mypy1": {
Name: "mypy1",
Version: "v0.770",
Language: pkg.Python,
Type: pkg.PythonSetupPkg,
Licenses: []string{},
Type: pkg.PythonPkg,
},
"mypy2": {
Name: "mypy2",
Version: "v0.770",
Language: pkg.Python,
Type: pkg.PythonSetupPkg,
Licenses: []string{},
Type: pkg.PythonPkg,
},
"mypy3": {
Name: "mypy3",
Version: "v0.770",
Language: pkg.Python,
Type: pkg.PythonSetupPkg,
Licenses: []string{},
Type: pkg.PythonPkg,
},
}
fixture, err := os.Open("test-fixtures/setup/setup.py")
@@ -55,6 +50,6 @@ func TestParseSetup(t *testing.T) {
t.Fatalf("failed to parse requirements: %+v", err)
}
assertPkgsEqual(t, actual, expected)
assertPackagesEqual(t, actual, expected)
}
-100
View File
@@ -1,100 +0,0 @@
package python
import (
"bufio"
"fmt"
"io"
"strings"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg"
)
// integrity check
var _ common.ParserFn = parseWheelMetadata
var _ common.ParserFn = parseEggMetadata
// parseWheelMetadata is a parser function for individual Python Wheel metadata file contents, returning all Python
// packages listed.
func parseWheelMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
packages, err := parseWheelOrEggMetadata(reader)
for idx := range packages {
packages[idx].Type = pkg.WheelPkg
}
return packages, err
}
// parseEggMetadata is a parser function for individual Python Egg metadata file contents, returning all Python
// packages listed.
func parseEggMetadata(_ string, reader io.Reader) ([]pkg.Package, error) {
packages, err := parseWheelOrEggMetadata(reader)
for idx := range packages {
packages[idx].Type = pkg.EggPkg
}
return packages, err
}
// parseWheelOrEggMetadata takes a Python Egg or Wheel (which share the same format and values for our purposes),
// returning all Python packages listed.
func parseWheelOrEggMetadata(reader io.Reader) ([]pkg.Package, error) {
fields := make(map[string]string)
var key string
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimRight(line, "\n")
// empty line indicates end of entry
if len(line) == 0 {
// if the entry has not started, keep parsing lines
if len(fields) == 0 {
continue
}
break
}
switch {
case strings.HasPrefix(line, " "):
// a field-body continuation
if len(key) == 0 {
return nil, fmt.Errorf("no match for continuation: line: '%s'", line)
}
val, ok := fields[key]
if !ok {
return nil, fmt.Errorf("no previous key exists, expecting: %s", key)
}
// concatenate onto previous value
val = fmt.Sprintf("%s\n %s", val, strings.TrimSpace(line))
fields[key] = val
default:
// parse a new key (note, duplicate keys are overridden)
if i := strings.Index(line, ":"); i > 0 {
key = strings.TrimSpace(line[0:i])
val := strings.TrimSpace(line[i+1:])
fields[key] = val
} else {
return nil, fmt.Errorf("cannot parse field from line: '%s'", line)
}
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to parse python wheel/egg: %w", err)
}
p := pkg.Package{
Name: fields["Name"],
Version: fields["Version"],
Language: pkg.Python,
}
if license, ok := fields["License"]; ok && license != "" {
p.Licenses = []string{license}
}
return []pkg.Package{p}, nil
}
@@ -0,0 +1,80 @@
package python
import (
"bufio"
"fmt"
"io"
"path/filepath"
"strings"
"github.com/anchore/stereoscope/pkg/file"
"github.com/mitchellh/mapstructure"
"github.com/anchore/syft/syft/pkg"
)
// parseWheelOrEggMetadata takes a Python Egg or Wheel (which share the same format and values for our purposes),
// returning all Python packages listed.
func parseWheelOrEggMetadata(path file.Path, reader io.Reader) (pkg.PythonPackageMetadata, error) {
fields := make(map[string]string)
var key string
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimRight(line, "\n")
// empty line indicates end of entry
if len(line) == 0 {
// if the entry has not started, keep parsing lines
if len(fields) == 0 {
continue
}
break
}
switch {
case strings.HasPrefix(line, " "):
// a field-body continuation
if len(key) == 0 {
return pkg.PythonPackageMetadata{}, fmt.Errorf("no match for continuation: line: '%s'", line)
}
val, ok := fields[key]
if !ok {
return pkg.PythonPackageMetadata{}, fmt.Errorf("no previous key exists, expecting: %s", key)
}
// concatenate onto previous value
val = fmt.Sprintf("%s\n %s", val, strings.TrimSpace(line))
fields[key] = val
default:
// parse a new key (note, duplicate keys are overridden)
if i := strings.Index(line, ":"); i > 0 {
// mapstruct cannot map keys with dashes, and we are expected to persist the "Author-email" field
key = strings.ReplaceAll(strings.TrimSpace(line[0:i]), "-", "")
val := strings.TrimSpace(line[i+1:])
fields[key] = val
} else {
return pkg.PythonPackageMetadata{}, fmt.Errorf("cannot parse field from line: '%s'", line)
}
}
}
if err := scanner.Err(); err != nil {
return pkg.PythonPackageMetadata{}, fmt.Errorf("failed to parse python wheel/egg: %w", err)
}
var metadata pkg.PythonPackageMetadata
if err := mapstructure.Decode(fields, &metadata); err != nil {
return pkg.PythonPackageMetadata{}, fmt.Errorf("unable to parse APK metadata: %w", err)
}
// add additional metadata not stored in the egg/wheel metadata file
sitePackagesRoot := filepath.Clean(filepath.Join(filepath.Dir(string(path)), ".."))
metadata.SitePackagesRootPath = sitePackagesRoot
return metadata, nil
}
@@ -0,0 +1,62 @@
package python
import (
"os"
"testing"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
func TestParseWheelEggMetadata(t *testing.T) {
tests := []struct {
Fixture string
ExpectedMetadata pkg.PythonPackageMetadata
}{
{
Fixture: "test-fixtures/egg-info/PKG-INFO",
ExpectedMetadata: pkg.PythonPackageMetadata{
Name: "requests",
Version: "2.22.0",
License: "Apache 2.0",
Platform: "UNKNOWN",
Author: "Kenneth Reitz",
AuthorEmail: "me@kennethreitz.org",
SitePackagesRootPath: "test-fixtures",
},
},
{
Fixture: "test-fixtures/dist-info/METADATA",
ExpectedMetadata: pkg.PythonPackageMetadata{
Name: "Pygments",
Version: "2.6.1",
License: "BSD License",
Platform: "any",
Author: "Georg Brandl",
AuthorEmail: "georg@python.org",
SitePackagesRootPath: "test-fixtures",
},
},
}
for _, test := range tests {
t.Run(test.Fixture, func(t *testing.T) {
fixture, err := os.Open(test.Fixture)
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseWheelOrEggMetadata(file.Path(test.Fixture), fixture)
if err != nil {
t.Fatalf("failed to parse: %+v", err)
}
for _, d := range deep.Equal(actual, test.ExpectedMetadata) {
t.Errorf("diff: %+v", d)
}
})
}
}
@@ -0,0 +1,60 @@
package python
import (
"encoding/csv"
"fmt"
"io"
"strings"
"github.com/anchore/syft/syft/pkg"
)
// parseWheelOrEggMetadata takes a Python Egg or Wheel (which share the same format and values for our purposes),
// returning all Python packages listed.
func parseWheelOrEggRecord(reader io.Reader) ([]pkg.PythonFileRecord, error) {
var records []pkg.PythonFileRecord
r := csv.NewReader(reader)
for {
recordList, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("unable to read python record file: %w", err)
}
if len(recordList) != 3 {
return nil, fmt.Errorf("python record an unexpected length=%d: %q", len(recordList), recordList)
}
var record pkg.PythonFileRecord
for idx, item := range recordList {
switch idx {
case 0:
record.Path = item
case 1:
if item == "" {
continue
}
fields := strings.Split(item, "=")
if len(fields) != 2 {
return nil, fmt.Errorf("unexpected python record digest: %q", item)
}
record.Digest = &pkg.Digest{
Algorithm: fields[0],
Value: fields[1],
}
case 2:
record.Size = item
}
}
records = append(records, record)
}
return records, nil
}
@@ -0,0 +1,57 @@
package python
import (
"os"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
func TestParseWheelEggRecord(t *testing.T) {
tests := []struct {
Fixture string
ExpectedMetadata []pkg.PythonFileRecord
}{
{
Fixture: "test-fixtures/egg-info/RECORD",
ExpectedMetadata: []pkg.PythonFileRecord{
{Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.Digest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"},
{Path: "requests/__init__.py", Digest: &pkg.Digest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"},
{Path: "requests/__pycache__/__version__.cpython-38.pyc"},
{Path: "requests/__pycache__/utils.cpython-38.pyc"},
{Path: "requests/__version__.py", Digest: &pkg.Digest{"sha256", "Bm-GFstQaFezsFlnmEMrJDe8JNROz9n2XXYtODdvjjc"}, Size: "436"},
{Path: "requests/utils.py", Digest: &pkg.Digest{"sha256", "LtPJ1db6mJff2TJSJWKi7rBpzjPS3mSOrjC9zRhoD3A"}, Size: "30049"},
},
},
{
Fixture: "test-fixtures/dist-info/RECORD",
ExpectedMetadata: []pkg.PythonFileRecord{
{Path: "../../../bin/pygmentize", Digest: &pkg.Digest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"},
{Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.Digest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"},
{Path: "Pygments-2.6.1.dist-info/RECORD"},
{Path: "pygments/__pycache__/__init__.cpython-38.pyc"},
{Path: "pygments/util.py", Digest: &pkg.Digest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},
},
},
}
for _, test := range tests {
t.Run(test.Fixture, func(t *testing.T) {
fixture, err := os.Open(test.Fixture)
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseWheelOrEggRecord(fixture)
if err != nil {
t.Fatalf("failed to parse: %+v", err)
}
for _, d := range deep.Equal(actual, test.ExpectedMetadata) {
t.Errorf("diff: %+v", d)
}
})
}
}
@@ -1,93 +0,0 @@
package python
import (
"os"
"testing"
"github.com/anchore/syft/syft/pkg"
)
func assertPkgsEqual(t *testing.T, actual []pkg.Package, expected map[string]pkg.Package) {
t.Helper()
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 {
expectedPkg, ok := expected[a.Name]
if !ok {
t.Errorf("unexpected package found: '%s'", a.Name)
}
if expectedPkg.Version != a.Version {
t.Errorf("unexpected package version: '%s'", a.Version)
}
if a.Language != expectedPkg.Language {
t.Errorf("bad language: '%+v'", a.Language)
}
if a.Type != expectedPkg.Type {
t.Errorf("bad package type: %+v", a.Type)
}
if len(a.Licenses) < len(expectedPkg.Licenses) {
t.Errorf("bad package licenses count: '%+v'", a.Licenses)
}
if len(a.Licenses) > 0 {
if a.Licenses[0] != expectedPkg.Licenses[0] {
t.Errorf("bad package licenses: '%+v'", a.Licenses)
}
}
}
}
func TestParseEggMetadata(t *testing.T) {
expected := map[string]pkg.Package{
"requests": {
Name: "requests",
Version: "2.22.0",
Language: pkg.Python,
Type: pkg.EggPkg,
Licenses: []string{"Apache 2.0"},
},
}
fixture, err := os.Open("test-fixtures/egg-info/PKG-INFO")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseEggMetadata(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse egg-info: %+v", err)
}
assertPkgsEqual(t, actual, expected)
}
func TestParseWheelMetadata(t *testing.T) {
expected := map[string]pkg.Package{
"Pygments": {
Name: "Pygments",
Version: "2.6.1",
Language: pkg.Python,
Type: pkg.WheelPkg,
Licenses: []string{"BSD License"},
},
}
fixture, err := os.Open("test-fixtures/dist-info/METADATA")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseWheelMetadata(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse dist-info: %+v", err)
}
assertPkgsEqual(t, actual, expected)
}
@@ -16,6 +16,6 @@ func (p PoetryMetadataPackage) Pkg() pkg.Package {
Name: p.Name,
Version: p.Version,
Language: pkg.Python,
Type: pkg.PoetryPkg,
Type: pkg.PythonPkg,
}
}
@@ -0,0 +1,5 @@
../../../bin/pygmentize,sha256=dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8,220
Pygments-2.6.1.dist-info/AUTHORS,sha256=PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY,8449
Pygments-2.6.1.dist-info/RECORD,,
pygments/__pycache__/__init__.cpython-38.pyc,,
pygments/util.py,sha256=586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA,10778
@@ -0,0 +1,2 @@
pygments
something_else
@@ -0,0 +1,6 @@
requests-2.22.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
requests/__init__.py,sha256=PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA,3921
requests/__pycache__/__version__.cpython-38.pyc,,
requests/__pycache__/utils.cpython-38.pyc,,
requests/__version__.py,sha256=Bm-GFstQaFezsFlnmEMrJDe8JNROz9n2XXYtODdvjjc,436
requests/utils.py,sha256=LtPJ1db6mJff2TJSJWKi7rBpzjPS3mSOrjC9zRhoD3A,30049
@@ -0,0 +1 @@
requests
@@ -0,0 +1,47 @@
Metadata-Version: 2.1
Name: Pygments
Version: 2.6.1
Summary: Pygments is a syntax highlighting package written in Python.
Home-page: https://pygments.org/
Author: Georg Brandl
Author-email: georg@python.org
License: BSD License
Keywords: syntax highlighting
Platform: any
Classifier: License :: OSI Approved :: BSD License
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: End Users/Desktop
Classifier: Intended Audience :: System Administrators
Classifier: Development Status :: 6 - Mature
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Operating System :: OS Independent
Classifier: Topic :: Text Processing :: Filters
Classifier: Topic :: Utilities
Requires-Python: >=3.5
Pygments
~~~~~~~~
Pygments is a syntax highlighting package written in Python.
It is a generic syntax highlighter suitable for use in code hosting, forums,
wikis or other applications that need to prettify source code. Highlights
are:
* a wide range of over 500 languages and other text formats is supported
* special attention is paid to details, increasing quality by a fair amount
* support for new languages and formats are added easily
* a number of output formats, presently HTML, LaTeX, RTF, SVG, all image formats that PIL supports and ANSI sequences
* it is usable as a command-line tool and as a library
:copyright: Copyright 2006-2019 by the Pygments team, see AUTHORS.
:license: BSD, see LICENSE for details.
+3 -29
View File
@@ -4,39 +4,13 @@ Package rpmdb provides a concrete Cataloger implementation for RPM "Package" DB
package rpmdb
import (
"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.RpmPkg Package Types defined in RPM DB files.
type Cataloger struct {
cataloger common.GenericCataloger
}
// New returns a new RPM DB cataloger object.
func New() *Cataloger {
// NewRpmdbCataloger returns a new RPM DB cataloger object.
func NewRpmdbCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/var/lib/rpm/Packages": parseRpmDB,
}
return &Cataloger{
cataloger: common.NewGenericCataloger(nil, globParsers),
}
}
// Name returns a string that uniquely describes this cataloger.
func (a *Cataloger) Name() string {
return "rpmdb-cataloger"
}
// SelectFiles returns a set of discovered RPM DB files from the user content source.
func (a *Cataloger) SelectFiles(resolver scope.FileResolver) []file.Reference {
return a.cataloger.SelectFiles(resolver)
}
// Catalog returns the Packages indexed from all RPM DB files discovered.
func (a *Cataloger) Catalog(contents map[file.Reference]string) ([]pkg.Package, error) {
return a.cataloger.Catalog(contents, a.Name())
return common.NewGenericCataloger(nil, globParsers, "rpmdb-cataloger")
}
+3 -2
View File
@@ -52,8 +52,9 @@ func parseRpmDB(_ string, reader io.Reader) ([]pkg.Package, error) {
Name: entry.Name,
Version: fmt.Sprintf("%s-%s", entry.Version, entry.Release), // this is what engine does
//Version: fmt.Sprintf("%d:%s-%s.%s", entry.Epoch, entry.Version, entry.Release, entry.Arch),
Type: pkg.RpmPkg,
Metadata: pkg.RpmMetadata{
Type: pkg.RpmPkg,
MetadataType: pkg.RpmdbMetadataType,
Metadata: pkg.RpmdbMetadata{
Name: entry.Name,
Version: entry.Version,
Epoch: entry.Epoch,
+8 -6
View File
@@ -1,19 +1,21 @@
package rpmdb
import (
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
"os"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
func TestParseRpmDB(t *testing.T) {
expected := map[string]pkg.Package{
"dive": {
Name: "dive",
Version: "0.9.2-1",
Type: pkg.RpmPkg,
Metadata: pkg.RpmMetadata{
Name: "dive",
Version: "0.9.2-1",
Type: pkg.RpmPkg,
MetadataType: pkg.RpmdbMetadataType,
Metadata: pkg.RpmdbMetadata{
Name: "dive",
Epoch: 0,
Arch: "x86_64",
+26
View File
@@ -0,0 +1,26 @@
/*
Package bundler provides a concrete Cataloger implementation for Ruby Gemfile.lock bundler files.
*/
package ruby
import (
"github.com/anchore/syft/syft/cataloger/common"
)
// NewGemFileLockCataloger returns a new Bundler cataloger object tailored for parsing index-oriented files (e.g. Gemfile.lock).
func NewGemFileLockCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/Gemfile.lock": parseGemFileLockEntries,
}
return common.NewGenericCataloger(nil, globParsers, "ruby-gemfile-cataloger")
}
// NewGemSpecCataloger returns a new Bundler cataloger object tailored for detecting installations of gems (e.g. Gemspec).
func NewGemSpecCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/specifications/*.gemspec": parseGemSpecEntries,
}
return common.NewGenericCataloger(nil, globParsers, "ruby-gemspec-cataloger")
}
@@ -1,4 +1,4 @@
package bundler
package ruby
import (
"bufio"
@@ -11,12 +11,12 @@ import (
)
// integrity check
var _ common.ParserFn = parseGemfileLockEntries
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) {
// 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)
@@ -44,7 +44,7 @@ func parseGemfileLockEntries(_ string, reader io.Reader) ([]pkg.Package, error)
Name: candidate[0],
Version: strings.Trim(candidate[1], "()"),
Language: pkg.Ruby,
Type: pkg.BundlerPkg,
Type: pkg.GemPkg,
})
}
}
@@ -0,0 +1,101 @@
package ruby
import (
"os"
"testing"
"github.com/anchore/syft/syft/pkg"
)
func TestParseGemfileLockEntries(t *testing.T) {
var expectedGems = 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",
}
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(expectedGems) {
for _, a := range actual {
t.Log(" ", a)
}
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(expectedGems))
}
for _, a := range actual {
expectedVersion, ok := expectedGems[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.GemPkg {
t.Errorf("bad package type (pkg=%+v): %+v", a.Name, a.Type)
}
}
}
+126
View File
@@ -0,0 +1,126 @@
package ruby
import (
"bufio"
"encoding/json"
"fmt"
"io"
"regexp"
"strings"
"github.com/anchore/syft/internal"
"github.com/mitchellh/mapstructure"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg"
)
// integrity check
var _ common.ParserFn = parseGemFileLockEntries
type postProcessor func(string) []string
// match example: Al\u003Ex ---> 003E
var unicodePattern = regexp.MustCompile(`\\u(?P<unicode>[0-9A-F]{4})`)
var patterns = map[string]*regexp.Regexp{
// match example: name = "railties".freeze ---> railties
"name": regexp.MustCompile(`.*\.name\s*=\s*["']{1}(?P<name>.*)["']{1} *`),
// match example: version = "1.0.4".freeze ---> 1.0.4
"version": regexp.MustCompile(`.*\.version\s*=\s*["']{1}(?P<version>.*)["']{1} *`),
// match example:
// homepage = "https://github.com/anchore/syft".freeze ---> https://github.com/anchore/syft
"homepage": regexp.MustCompile(`.*\.homepage\s*=\s*["']{1}(?P<homepage>.*)["']{1} *`),
// match example: files = ["exe/bundle".freeze, "exe/bundler".freeze] ---> "exe/bundle".freeze, "exe/bundler".freeze
"files": regexp.MustCompile(`.*\.files\s*=\s*\[(?P<files>.*)\] *`),
// match example: authors = ["Andr\u00E9 Arko".freeze, "Samuel Giddins".freeze, "Colby Swandale".freeze,
// "Hiroshi Shibata".freeze, "David Rodr\u00EDguez".freeze, "Grey Baker".freeze...]
"authors": regexp.MustCompile(`.*\.authors\s*=\s*\[(?P<authors>.*)\] *`),
// match example: licenses = ["MIT".freeze] ----> "MIT".freeze
"licenses": regexp.MustCompile(`.*\.licenses\s*=\s*\[(?P<licenses>.*)\] *`),
}
var postProcessors = map[string]postProcessor{
"files": processList,
"authors": processList,
"licenses": processList,
}
func processList(s string) []string {
// nolint:prealloc
var results []string
for _, item := range strings.Split(s, ",") {
results = append(results, strings.Trim(item, "\" "))
}
return results
}
func parseGemSpecEntries(_ string, reader io.Reader) ([]pkg.Package, error) {
var pkgs []pkg.Package
var fields = make(map[string]interface{})
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
sanitizedLine := strings.TrimSpace(line)
sanitizedLine = strings.ReplaceAll(sanitizedLine, ".freeze", "")
sanitizedLine = renderUtf8(sanitizedLine)
if sanitizedLine == "" {
continue
}
for field, pattern := range patterns {
matchMap := internal.MatchCaptureGroups(pattern, sanitizedLine)
if value := matchMap[field]; value != "" {
if postProcessor := postProcessors[field]; postProcessor != nil {
fields[field] = postProcessor(value)
} else {
fields[field] = value
}
// TODO: know that a line could actually match on multiple patterns, this is unlikely though
break
}
}
}
if fields["name"] != "" && fields["version"] != "" {
var metadata pkg.GemMetadata
if err := mapstructure.Decode(fields, &metadata); err != nil {
return nil, fmt.Errorf("unable to decode gem metadata: %w", err)
}
pkgs = append(pkgs, pkg.Package{
Name: metadata.Name,
Version: metadata.Version,
Licenses: metadata.Licenses,
Language: pkg.Ruby,
Type: pkg.GemPkg,
MetadataType: pkg.GemMetadataType,
Metadata: metadata,
})
}
return pkgs, nil
}
// renderUtf8 takes any string escaped string sub-sections from the ruby string and replaces those sections with the UTF8 runes.
func renderUtf8(s string) string {
fullReplacement := unicodePattern.ReplaceAllStringFunc(s, func(unicodeSection string) string {
var replacement string
// note: the json parser already has support for interpreting hex-representations of unicode escaped strings as unicode runes.
// we can do this ourselves with strconv.Atoi, or leverage the existing json package.
if err := json.Unmarshal([]byte(`"`+unicodeSection+`"`), &replacement); err != nil {
return unicodeSection
}
return replacement
})
return fullReplacement
}
+49
View File
@@ -0,0 +1,49 @@
package ruby
import (
"os"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
func TestParseGemspec(t *testing.T) {
var expectedPkg = pkg.Package{
Name: "bundler",
Version: "2.1.4",
Type: pkg.GemPkg,
Licenses: []string{"MIT"},
Language: pkg.Ruby,
MetadataType: pkg.GemMetadataType,
Metadata: pkg.GemMetadata{
Name: "bundler",
Version: "2.1.4",
Files: []string{"exe/bundle", "exe/bundler"},
Authors: []string{"André Arko", "Samuel Giddins", "Colby Swandale", "Hiroshi Shibata", "David Rodríguez", "Grey Baker", "Stephanie Morillo", "Chris Morris", "James Wen", "Tim Moore", "André Medeiros", "Jessica Lynn Suttles", "Terence Lee", "Carl Lerche", "Yehuda Katz"},
Licenses: []string{"MIT"},
Homepage: "https://bundler.io",
},
}
fixture, err := os.Open("test-fixtures/bundler.gemspec")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
actual, err := parseGemSpecEntries(fixture.Name(), fixture)
if err != nil {
t.Fatalf("failed to parse gemspec: %+v", err)
}
if len(actual) != 1 {
for _, a := range actual {
t.Log(" ", a)
}
t.Fatalf("unexpected package count: %d!=1", len(actual))
}
for _, d := range deep.Equal(actual[0], expectedPkg) {
t.Errorf("diff: %+v", d)
}
}
@@ -0,0 +1,26 @@
# frozen_string_literal: true
# -*- encoding: utf-8 -*-
# stub: bundler 2.1.4 ruby lib
Gem::Specification.new do |s|
s.name = "bundler".freeze
s.version = "2.1.4"
s.required_rubygems_version = Gem::Requirement.new(">= 2.5.2".freeze) if s.respond_to? :required_rubygems_version=
s.metadata = { "bug_tracker_uri" => "https://github.com/bundler/bundler/issues", "changelog_uri" => "https://github.com/bundler/bundler/blob/master/CHANGELOG.md", "homepage_uri" => "https://bundler.io/", "source_code_uri" => "https://github.com/bundler/bundler/" } if s.respond_to? :metadata=
s.require_paths = ["lib".freeze]
s.authors = ["Andr\u00E9 Arko".freeze, "Samuel Giddins".freeze, "Colby Swandale".freeze, "Hiroshi Shibata".freeze, "David Rodr\u00EDguez".freeze, "Grey Baker".freeze, "Stephanie Morillo".freeze, "Chris Morris".freeze, "James Wen".freeze, "Tim Moore".freeze, "Andr\u00E9 Medeiros".freeze, "Jessica Lynn Suttles".freeze, "Terence Lee".freeze, "Carl Lerche".freeze, "Yehuda Katz".freeze]
s.bindir = "exe".freeze
s.date = "2020-01-05"
s.description = "Bundler manages an application's dependencies through its entire life, across many machines, systematically and repeatably".freeze
s.email = ["team@bundler.io".freeze]
s.executables = ["bundle".freeze, "bundler".freeze]
s.files = ["exe/bundle".freeze, "exe/bundler".freeze]
s.homepage = "https://bundler.io".freeze
s.licenses = ["MIT".freeze]
s.required_ruby_version = Gem::Requirement.new(">= 2.3.0".freeze)
s.rubygems_version = "3.1.2".freeze
s.summary = "The best way to manage your application's dependencies".freeze
s.installed_by_version = "3.1.2" if s.respond_to? :installed_by_version
end
+11 -1
View File
@@ -20,6 +20,9 @@ func NewUnknownDistro() Distro {
}
func NewDistro(t Type, ver string) (Distro, error) {
if ver == "" {
return Distro{Type: t}, nil
}
verObj, err := hashiVer.NewVersion(ver)
if err != nil {
return Distro{}, fmt.Errorf("could not create distro version: %w", err)
@@ -32,6 +35,9 @@ func NewDistro(t Type, ver string) (Distro, error) {
}
func (d Distro) MajorVersion() string {
if d.Version == nil {
return fmt.Sprint("(version unknown)")
}
return fmt.Sprintf("%d", d.Version.Segments()[0])
}
@@ -40,7 +46,11 @@ func (d Distro) FullVersion() string {
}
func (d Distro) String() string {
return fmt.Sprintf("%s %s", d.Type, d.RawVersion)
versionStr := "(version unknown)"
if d.RawVersion != "" {
versionStr = d.RawVersion
}
return fmt.Sprintf("%s %s", d.Type, versionStr)
}
// Name provides a string repr of the distro
+2 -1
View File
@@ -48,6 +48,7 @@ identifyLoop:
}
if len(refs) == 0 {
log.Debugf("No Refs found from path: %s", entry.path)
continue
}
@@ -84,7 +85,7 @@ func assemble(name, version string) *Distro {
distroType, ok := IDMapping[name]
// Both distro and version must be present
if len(name) == 0 || len(version) == 0 {
if len(name) == 0 {
return nil
}
+14 -4
View File
@@ -65,6 +65,15 @@ func TestIdentifyDistro(t *testing.T) {
fixture: "test-fixtures/os/unmatchable",
Type: UnknownDistroType,
},
{
fixture: "test-fixtures/os/opensuse-leap",
Type: OpenSuseLeap,
Version: "15.2.0",
},
{
fixture: "test-fixtures/os/arch",
Type: ArchLinux,
},
}
observedDistros := internal.NewStringSet()
@@ -94,6 +103,11 @@ func TestIdentifyDistro(t *testing.T) {
return
}
if d.Version == nil {
t.Log("Distro doesn't have a Version")
return
}
if d.Version.String() != test.Version {
t.Errorf("expected distro version doesn't match: %v != %v", d.Version.String(), test.Version)
}
@@ -175,10 +189,6 @@ func TestParseOsReleaseFailures(t *testing.T) {
fixture string
name string
}{
{
fixture: "test-fixtures/bad-version",
name: "No version",
},
{
fixture: "test-fixtures/bad-id",
name: "No name ID",
-15
View File
@@ -1,15 +0,0 @@
NAME="Red Hat Enterprise Linux"
VERSION="8.1 (Ootpa)"
ID="rhel"
ID_LIKE="fedora"
PLATFORM_ID="platform:el8"
PRETTY_NAME="Red Hat Enterprise Linux 8.1 (Ootpa)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:redhat:enterprise_linux:8.1:GA"
HOME_URL="https://www.redhat.com/"
BUG_REPORT_URL="https://bugzilla.redhat.com/"
REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 8"
REDHAT_BUGZILLA_PRODUCT_VERSION=8.1
REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux"
REDHAT_SUPPORT_PRODUCT_VERSION="8.1"
@@ -0,0 +1,10 @@
NAME="Arch Linux"
PRETTY_NAME="Arch Linux"
ID=arch
BUILD_ID=rolling
ANSI_COLOR="38;2;23;147;209"
HOME_URL="https://www.archlinux.org/"
DOCUMENTATION_URL="https://wiki.archlinux.org/"
SUPPORT_URL="https://bbs.archlinux.org/"
BUG_REPORT_URL="https://bugs.archlinux.org/"
LOGO=archlinux

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