Compare commits

...
49 Commits
Author SHA1 Message Date
Keith Zantow ac94bf530c fix: update graalvm cataloger to fix panic (#1454)
Fixes https://github.com/anchore/syft/issues/1453
2023-01-12 17:42:13 -05:00
Weston Steimel e87cfe7319 chore: remove bumping cosign in go.mod when updating bootstrap tools (#1452) 2023-01-12 16:21:01 -05:00
Asi GreenholtsandAlex Goodman 260cb4c72d feat: Add the origin field to the output format of syftjson (#1327)
* moved the relevant fields to the Metadata field

Signed-off-by: Asaf Greenholts <asaf@cidersecurity.io>

* added metadata types

Signed-off-by: Asaf Greenholts <asaf@cidersecurity.io>

* Added hashes to metadata of packge-lock.json and Pipfile.lock

Signed-off-by: Asaf Greenholts <asaf@cidersecurity.io>

* move package metadata types to "pkg" package

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

* re-generate json schema to include new npm, python, and binary metadatas

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

Signed-off-by: Asaf Greenholts <asaf@cidersecurity.io>
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2023-01-12 15:03:05 -05:00
Keith Zantow 85bddaa43d chore: update schema (#1449) 2023-01-12 14:25:47 -05:00
Arnout EngelenandAlex Goodman a864dc9505 feat: prefer known CPE vendors over other candidates (#1294)
* feat: prefer known CPE vendors over other candidates

All ASF projects will be under the `apache` vendor in CPE, and
indeed this is already one of the candidates, but the logic
for selecting the 'most specific' CPE string would select for
example `apache_software_foundation` or `commons-text`.

This is not necessarily 'wrong' in the CPE candidate selection
logic: there is no way to reliably determine the right candidate.
I think it makes sense to use specific data around the vendor
candidate generation, somewhat similar to
'defaultCandidateAdditions'.

Unfortunately there are still a few CVE's for old (pre-5.x,
long unsupported) tomcat versions that are actually tagged with
`apache_software_foundation`, but I'm not sure those are worth
spending time on.

Signed-off-by: Arnout Engelen <arnout@bzzt.net>

* chore: swap out array of vendors for set data structure

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

Signed-off-by: Arnout Engelen <arnout@bzzt.net>
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2023-01-12 19:16:53 +00:00
Christopher Angelo PhillipsandAlex Goodman 44e8ae2577 fix: update attestation code to remove library dependencies and shellout for keyless flow (#1442)
Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2023-01-12 17:22:05 +00:00
Chapman PenderyandAlex Goodman ac8f72fdd1 feat: add BeamVM Hex support (#1073)
* feat: initial commit providing mix support

Signed-off-by: cpendery <cpendery@vt.edu>

* feat: add rebar parser

Signed-off-by: cpendery <cpendery@vt.edu>

* fix: add beam/hex everywhere else required for Syft runtime

Signed-off-by: cpendery <cpendery@vt.edu>

* style: fix lints

Signed-off-by: cpendery <cpendery@vt.edu>

* ci: fix failing tests

Signed-off-by: cpendery <cpendery@vt.edu>

* docs: update with new supported languages

Signed-off-by: cpendery <cpendery@vt.edu>

* chore: update elixir/erlang catalogers to generic cataloger

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

Signed-off-by: cpendery <cpendery@vt.edu>
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2023-01-12 12:10:46 -05:00
witchcraze e063471c66 feat: add apache httpd binary classifier (#1448)
Signed-off-by: witchcraze <witchcraze@gmail.com>
2023-01-12 10:50:01 -05:00
Batuhan Apaydın 645debe7a4 chore: claim artifacthub package ownership from developer-guy (#881)
Signed-off-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com>

Signed-off-by: Batuhan Apaydın <batuhan.apaydin@trendyol.com>
2023-01-11 15:25:42 -05:00
mikclandAlex Goodman 4bfb849310 Parallel package catalog processing (#1355)
* catalog: run cataloggers concurrently

Signed-off-by: mikcl <mikesmikes400@gmail.com>

* frontend: expose workers as a configurable option

Signed-off-by: mikcl <mikesmikes400@gmail.com>

* fixup! frontend: expose workers as a configurable option

Signed-off-by: mikcl <mikesmikes400@gmail.com>

* update logging statements

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

* test: assert for debug logging

Signed-off-by: mikcl <mikesmikes400@gmail.com>

Signed-off-by: mikcl <mikesmikes400@gmail.com>
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
Co-authored-by: Alex Goodman <alex.goodman@anchore.com>
2023-01-11 15:18:02 -05:00
witchcraze d524bd5fc3 feat: Add php binary catalogers (#1444)
* add php classifier
Signed-off-by: witchcraze <witchcraze@gmail.com>

* make lint-fix
Signed-off-by: witchcraze <witchcraze@gmail.com>
2023-01-11 13:46:20 -05:00
anchore-actions-token-generator[bot]andspiffcs a8416d674b Update syft bootstrap tools to latest versions. (#1443)
Co-authored-by: spiffcs <spiffcs@users.noreply.github.com>
2023-01-11 12:50:40 -05:00
Keith Zantow 725529f43f fix: duplicate file in tar archive causes read to fail (#1445) 2023-01-10 14:55:02 -05:00
William Blair e480443c8c Add support for GraalVM Native Image executables. (#1276)
Signed-off-by: William Blair <william.blair@oracle.com>
2023-01-06 18:31:22 -05:00
Benji Visser db386baf81 Add redis binary classifier (#1438)
Signed-off-by: Benji Visser <benji@093b.org>
2023-01-06 12:50:48 -05:00
Christopher Angelo Phillips 795a63f1c9 docs: add cataloger construction summary (#1434) 2023-01-05 17:03:00 +00:00
anchore-actions-token-generator[bot] d4f9993b8d chore: update bootstrap tools to latest versions. (#1428) 2023-01-05 10:20:58 -05:00
Benji Visser bb6fc6525c Add alpine type to purl (#1431)
Signed-off-by: Benji Visser <benji@093b.org>
2023-01-04 17:35:46 -05:00
Benji Visser bc1edb9c8a adding purl types for binary classifiers (#1435)
Signed-off-by: Benji Visser <benji@093b.org>
2023-01-04 11:34:37 -05:00
Keith Zantow 64be0a1072 chore: refactor basic CPE functionality to its own package (#1436) 2023-01-04 11:26:28 -05:00
Justin Chadwell e3d6ffd30e fix: typo in os.Getwd error message (#1433)
Signed-off-by: Justin Chadwell <me@jedevc.com>
2023-01-03 14:56:20 +00:00
Justin Chadwell 8d36b21237 fix: additional excessive go binary warnings (#1432)
The original fix b125ea83ba didn't catch
all the excessive warnings, it seems like getArches can also be called
on binaries that aren't neccessarily go binaries, so the messages from
this should also be Trace instead of Warn.

Signed-off-by: Justin Chadwell <me@jedevc.com>
2023-01-03 09:54:08 -05:00
Rui Chen 6a7d6e6071 docs: migrate to homebrew-core (#1427) 2023-01-02 08:16:32 -05:00
Keith Zantow e1e489a284 fix: unicode output in cyclonedx-json format (#1420) 2022-12-23 08:37:47 -05:00
Keith Zantow b125ea83ba fix: excessive go binary warnings (#1424) 2022-12-23 08:36:49 -05:00
Christopher Angelo Phillips 3690f979b3 feat: update spdx format model to produce valid spdx json documents (#1418) 2022-12-21 15:56:03 -05:00
Alex Goodman 5dd726fc86 clean package names in python parsers (#1417)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-12-21 18:31:49 +00:00
Christopher Angelo Phillips c8b8b1ca11 docs: update schema name to 2.3 (#1416) 2022-12-20 02:57:19 +00:00
Keith Zantow 7b08608adb feat: add h1digest when scanning go.mod (#1405)
Fixes https://github.com/anchore/syft/issues/1277
2022-12-20 02:18:35 +00:00
dja-fr 82f32c7301 feat: Add license parsing for java (#1385) 2022-12-19 20:10:15 -05:00
Keith Zantow 4ffbeeeea5 fix: cyclonedx component type for binaries (#1406) 2022-12-19 19:49:27 -05:00
Keith Zantow b1d6dae203 fix: openjdk detection pattern (#1415) 2022-12-19 19:49:04 -05:00
Christopher Angelo Phillips 0f1e8fca14 bug: spdx checksum empty array; allow syft to generate SHA1 for spdx-tag-value documents (#1404) 2022-12-20 00:10:35 +00:00
Thomas Klausner 8b38549b79 Add NetBSD support. (#1412) 2022-12-19 16:59:50 -05:00
Christopher Angelo Phillips 23a3173c9f feat: add catalog delete (#1377) 2022-12-12 12:55:12 -05:00
Keith Zantow 17aa8287e6 docs: remove file classifier (#1397) 2022-12-08 16:50:29 +00:00
Christopher Angelo Phillips 730d3e3187 chore: update latest cyclonedx library (#1390) 2022-12-08 11:36:08 -05:00
Keith Zantow 997fbdfcf3 feat: Add Java binary catalogers (#1392) 2022-12-08 10:50:28 -05:00
Marc-Etienne Vargenau 13ceed9336 chore: Update SPDX license list to 3.19 (#1389) 2022-12-08 10:29:27 -05:00
Chapman Pendery 668f102340 fix: add manual vendor/product removal to fix false flags (#1070)
Closes https://github.com/anchore/syft/issues/1066
Closes https://github.com/anchore/grype/issues/800
Closes https://github.com/anchore/grype/issues/491
2022-12-08 09:57:42 -05:00
anchore-actions-token-generator[bot] f1a124209a Update Stereoscope to c5ff155d72f166e2332e160a75c3ff2b8e9c7e2e (#1395)
Signed-off-by: GitHub <noreply@github.com>
2022-12-08 08:32:49 +00:00
Keith Zantow 5dbb3fc41d chore: fix test busybox image sha (#1393) 2022-12-07 20:15:39 -05:00
Keith Zantow 614ea00905 fix: go version not properly identified in binary (#1384) 2022-12-02 13:24:36 -05:00
anchore-actions-token-generator[bot]andwagoodman 247b054ab5 Update Stereoscope to 3b80d983223f6e6fc2d33b0ffa003d30268418e9 (#1376)
Signed-off-by: GitHub <noreply@github.com>

Signed-off-by: GitHub <noreply@github.com>
Co-authored-by: wagoodman <wagoodman@users.noreply.github.com>
2022-11-30 16:11:57 +00:00
Keith Zantow 9e43725951 fix: Update node binary package name (#1375) 2022-11-30 10:30:57 -05:00
Keith Zantow 4f39287216 feat: Generic Binary Cataloger (#1336) 2022-11-29 18:28:10 -05:00
Alex Goodman 7a69e2129b recover from bad parsing of golang binary (#1371)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
2022-11-29 15:56:46 +00:00
Dan Luhring f6996f7b9a Fix parsing of apk databases with large entries (#1365)
Closes https://github.com/anchore/syft/issues/1354
2022-11-29 10:16:36 -05:00
anchore-actions-token-generator[bot]andspiffcs bd523bdb5d Update syft bootstrap tools to latest versions. (#1369)
Co-authored-by: spiffcs <spiffcs@users.noreply.github.com>
2022-11-29 10:13:00 -05:00
234 changed files with 186241 additions and 10330 deletions
+1 -5
View File
@@ -39,10 +39,6 @@ jobs:
sed -r -i -e 's/^(YAJSV_VERSION = ).*/\1'${YAJSV_LATEST_VERSION}'/' Makefile
sed -r -i -e 's/^(COSIGN_VERSION = ).*/\1'${COSIGN_LATEST_VERSION}'/' Makefile
# update cosign in go.mod as well
go get github.com/sigstore/cosign@$COSIGN_LATEST_VERSION
go mod tidy
# export the versions for use with create-pull-request
echo "::set-output name=GOLANGCILINT::$GOLANGCILINT_LATEST_VERSION"
echo "::set-output name=BOUNCER::$BOUNCER_LATEST_VERSION"
@@ -76,4 +72,4 @@ jobs:
- [yajsv ${{ steps.latest-versions.outputs.YAJSV }}](https://github.com/neilpa/yajsv/releases/tag/${{ steps.latest-versions.outputs.YAJSV }})
- [cosign ${{ steps.latest-versions.outputs.COSIGN }}](https://github.com/sigstore/cosign/releases/tag/${{ steps.latest-versions.outputs.COSIGN }})
This is an auto-generated pull request to update all of the bootstrap tools to the latest versions.
token: ${{ steps.generate-token.outputs.token }}
token: ${{ steps.generate-token.outputs.token }}
+7
View File
@@ -0,0 +1,7 @@
# Syft Contributors
The following Syft components were contributed by external authors/organizations.
## GraalVM Native Image
A cataloger contributed by Oracle Corporation that extracts packages given within GraalVM Native Image SBOMs.
+42
View File
@@ -132,6 +132,48 @@ sequenceDiagram
```
### Syft Catalogers
##### Summary
Catalogers are the way in which syft is able to identify and construct packages given some amount of source metadata.
For example, Syft can locate and process `package-lock.json` files when performing filesystem scans.
See: [how to specify file globs](https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/javascript/cataloger.go#L16-L21)
and an implementation of the [package-lock.json parser](https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/javascript/cataloger.go#L16-L21) fora quick review.
#### Building a new Cataloger
Catalogers must fulfill the interface [found here](https://github.com/anchore/syft/blob/main/syft/pkg/cataloger.go).
This means that when building a new cataloger, the new struct must implement both method signatures of `Catalog` and `Name`.
A top level view of the functions that construct all the catalogers can be found [here](https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/cataloger.go).
When an author has finished writing a new cataloger this is the spot to plug in the new catalog constructor.
For a top level view of how the catalogers are used see [this function](https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/catalog.go#L41-L100) as a reference. It ranges over all catalogers passed as an argument and invokes the `Catalog` method:
Each cataloger has its own `Catalog` method, but this does not mean that they are all vastly different.
Take a look at the `apkdb` cataloger for alpine to see how it [constructs a generic.NewCataloger](https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/apkdb/cataloger.go).
`generic.NewCataloger` is an abstraction syft uses to make writing common components easier. First, it takes the `catalogerName` to identify the cataloger.
On the other side of the call it uses two key pieces which inform the cataloger how to identify and return packages, the `globPatterns` and the `parseFunction`:
- The first piece is a `parseByGlob` matching pattern used to identify the files that contain the package metadata.
See [here for the APK example](https://github.com/anchore/syft/blob/main/syft/pkg/apk_metadata.go#L16-L41).
- The other is a `parseFunction` which informs the cataloger what to do when it has found one of the above matches files.
See this [link for an example](https://github.com/anchore/syft/blob/main/syft/pkg/cataloger/apkdb/parse_apk_db.go#L22-L102).
If you're unsure about using the `Generic Cataloger` and think the use case being filled requires something more custom
just file an issue or ask in our slack, and we'd be more than happy to help on the design.
Identified packages share a common struct so be sure that when the new cataloger is constructing a new package it is using the [`Package` struct](https://github.com/anchore/syft/blob/main/syft/pkg/package.go#L16-L31).
Metadata Note: Identified packages are also assigned specific metadata that can be unique to their environment.
See [this folder](https://github.com/anchore/syft/tree/main/syft/pkg) for examples of the different metadata types.
These are plugged into the `MetadataType` and `Metadata` fields in the above struct. `MetadataType` informs which type is being used. `Metadata` is an interface converted to that type.
Finally, here is an example of where the package construction is done in the apk cataloger. The first link is where `newPackage` is called in the `parseFunction`. The second link shows the package construction:
- [Call for new package](https://github.com/anchore/syft/blob/6a7d6e6071829c7ce2943266c0e187b27c0b325c/syft/pkg/cataloger/apkdb/parse_apk_db.go#L96-L99)
- [APK Package Constructor](https://github.com/anchore/syft/blob/6a7d6e6071829c7ce2943266c0e187b27c0b325c/syft/pkg/cataloger/apkdb/package.go#L12-L27)
If you have more questions about implementing a cataloger or questions about one you might be currently working
always feel free to file an issue or reach out to us [on slack](https://anchore.com/slack).
## Testing
+2 -2
View File
@@ -10,10 +10,10 @@ SNAPSHOT_CMD=$(RELEASE_CMD) --skip-publish --snapshot
# tool versions
GOLANGCILINT_VERSION = v1.50.1
GOSIMPORTS_VERSION = v0.3.4
GOSIMPORTS_VERSION = v0.3.5
BOUNCER_VERSION = v0.4.0
CHRONICLE_VERSION = v0.4.2
GORELEASER_VERSION = v1.13.0
GORELEASER_VERSION = v1.14.1
YAJSV_VERSION = v1.4.1
COSIGN_VERSION = v1.13.1
QUILL_VERSION = v0.2.0
+8 -13
View File
@@ -38,9 +38,11 @@ For commercial support options with Syft or Grype, please [contact Anchore](http
- Debian (dpkg)
- Dotnet (deps.json)
- Objective-C (cocoapods)
- Elixir (mix)
- Erlang (rebar3)
- Go (go.mod, Go binaries)
- Haskell (cabal, stack)
- Java (jar, ear, war, par, sar)
- Java (jar, ear, war, par, sar, native-image)
- JavaScript (npm, yarn)
- Jenkins Plugins (jpi, hpi)
- PHP (composer)
@@ -75,7 +77,6 @@ choco install syft -y
### Homebrew
```bash
brew tap anchore/syft
brew install syft
```
@@ -366,6 +367,8 @@ syft convert sbom.syft.json -o cyclonedx-json=sbom.cdx.json # convert it to Cyc
### Keyless support
Syft supports generating attestations using cosign's [keyless](https://github.com/sigstore/cosign/blob/main/KEYLESS.md) signatures.
Note: users need to have >= v1.12.0 of cosign installed for this command to function
To use this feature with a format like CycloneDX json simply run:
```
syft attest --output cyclonedx-json <IMAGE WITH OCI WRITE ACCESS>
@@ -489,17 +492,6 @@ package:
# same as -s ; SYFT_PACKAGE_CATALOGER_SCOPE env var
scope: "squashed"
# cataloging file classifications is exposed through the power-user subcommand
file-classification:
cataloger:
# enable/disable cataloging of file classifications
# SYFT_FILE_CLASSIFICATION_CATALOGER_ENABLED env var
enabled: true
# the search space to look for file classifications (options: all-layers, squashed)
# SYFT_FILE_CLASSIFICATION_CATALOGER_SCOPE env var
scope: "squashed"
# cataloging file contents is exposed through the power-user subcommand
file-contents:
cataloger:
@@ -534,6 +526,9 @@ file-metadata:
# SYFT_FILE_METADATA_DIGESTS env var
digests: ["sha256"]
# maximum number of workers used to process the list of package catalogers in parallel
parallelism: 1
# cataloging secrets is exposed through the power-user subcommand
secrets:
cataloger:
+1 -1
View File
@@ -1,5 +1,5 @@
# See documentation here: https://github.com/artifacthub/hub/blob/v1.6.0/docs/metadata/artifacthub-repo.yml
repositoryID: eced152f-b15d-4879-8b3b-1175397192ba
owners:
- name: wagoodman
email: wagoodman@gmail.com
+12 -32
View File
@@ -4,7 +4,6 @@ import (
"fmt"
"log"
sigopts "github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -15,33 +14,27 @@ import (
)
const (
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] --key [KEY] alpine:latest
Supports the following image sources:
{{.appName}} {{.command}} --key [KEY] yourrepo/yourimage:tag defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry.
{{.appName}} {{.command}} --key [KEY] path/to/a/file/or/dir only for OCI tar or OCI directory
attestExample = ` {{.appName}} {{.command}} --output [FORMAT] alpine:latest defaults to using images from a Docker daemon. If Docker is not present, the image is pulled directly from the registry
`
attestSchemeHelp = "\n" + indent + schemeHelpHeader + "\n" + imageSchemeHelp
attestHelp = attestExample + attestSchemeHelp
attestHelp = attestExample + attestSchemeHelp
)
func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions) *cobra.Command {
ao := options.AttestOptions{}
//nolint:dupl
func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions, po *options.PackagesOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "attest --output [FORMAT] --key [KEY] [SOURCE]",
Short: "Generate a package SBOM as an attestation for the given [SOURCE] container image",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation",
Use: "attest --output [FORMAT] <IMAGE>",
Short: "Generate an SBOM as an attestation for the given [SOURCE] container image",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from a container image as the predicate of an in-toto attestation that will be uploaded to the image registry",
Example: internal.Tprintf(attestHelp, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "attest",
}),
Args: func(cmd *cobra.Command, args []string) error {
// run to unmarshal viper object onto app config
// the viper object correctly
if err := app.LoadAllValues(v, ro.Config); err != nil {
return fmt.Errorf("invalid application config: %w", err)
return fmt.Errorf("unable to load configuration: %w", err)
}
// configure logging for command
newLogWrapper(app)
logApplicationConfig(app)
return validateArgs(cmd, args)
@@ -49,29 +42,16 @@ func Attest(v *viper.Viper, app *config.Application, ro *options.RootOptions) *c
SilenceUsage: true,
SilenceErrors: true,
RunE: func(cmd *cobra.Command, args []string) error {
// this MUST be called first to make sure app config decodes
// the viper object correctly
if app.CheckForAppUpdate {
checkForApplicationUpdate()
}
// build cosign key options for attestation
ko := sigopts.KeyOpts{
KeyRef: app.Attest.KeyRef,
FulcioURL: app.Attest.FulcioURL,
IDToken: app.Attest.FulcioIdentityToken,
InsecureSkipFulcioVerify: app.Attest.InsecureSkipFulcioVerify,
RekorURL: app.Attest.RekorURL,
OIDCIssuer: app.Attest.OIDCIssuer,
OIDCClientID: app.Attest.OIDCClientID,
OIDCRedirectURL: app.Attest.OIDCRedirectURL,
}
return attest.Run(cmd.Context(), app, ko, args)
return attest.Run(cmd.Context(), app, args)
},
}
err := ao.AddFlags(cmd, v)
// syft attest is an enhancment of the packages command, so it should have the same flags
err := po.AddFlags(cmd, v)
if err != nil {
log.Fatal(err)
}
+134 -305
View File
@@ -1,105 +1,60 @@
package attest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/in-toto/in-toto-golang/in_toto"
sigopts "github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/attestation"
cbundle "github.com/sigstore/cosign/pkg/cosign/bundle"
"github.com/sigstore/cosign/pkg/oci/mutate"
ociremote "github.com/sigstore/cosign/pkg/oci/remote"
"github.com/sigstore/cosign/pkg/oci/static"
sigs "github.com/sigstore/cosign/pkg/signature"
"github.com/sigstore/cosign/pkg/types"
"github.com/sigstore/rekor/pkg/generated/client"
"github.com/sigstore/rekor/pkg/generated/models"
"github.com/sigstore/sigstore/pkg/signature/dsse"
signatureoptions "github.com/sigstore/sigstore/pkg/signature/options"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
"golang.org/x/exp/slices"
"github.com/anchore/stereoscope"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/syft/cmd/syft/cli/eventloop"
"github.com/anchore/syft/cmd/syft/cli/options"
"github.com/anchore/syft/cmd/syft/cli/packages"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/formats/cyclonedxjson"
"github.com/anchore/syft/syft/formats/spdxjson"
"github.com/anchore/syft/syft/event/monitor"
"github.com/anchore/syft/syft/formats/syftjson"
"github.com/anchore/syft/syft/formats/table"
"github.com/anchore/syft/syft/formats/template"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
var (
allowedAttestFormats = []sbom.FormatID{
syftjson.ID,
spdxjson.ID,
cyclonedxjson.ID,
}
intotoJSONDsseType = `application/vnd.in-toto+json`
)
func Run(ctx context.Context, app *config.Application, ko sigopts.KeyOpts, args []string) error {
// We cannot generate an attestation for more than one output
if len(app.Outputs) > 1 {
return fmt.Errorf("unable to generate attestation for more than one output")
}
// can only be an image for attestation or OCI DIR
userInput := args[0]
si, err := parseImageSource(userInput, app)
func Run(ctx context.Context, app *config.Application, args []string) error {
err := ValidateOutputOptions(app)
if err != nil {
return err
}
output := parseAttestationOutput(app.Outputs)
format := syft.FormatByName(output)
// user typo or unknown outputs provided
if format == nil {
format = syft.FormatByID(syftjson.ID) // default attestation format
}
predicateType := formatPredicateType(format)
if predicateType == "" {
return fmt.Errorf(
"could not produce attestation predicate for given format: %q. Available formats: %+v",
options.FormatAliases(format.ID()),
options.FormatAliases(allowedAttestFormats...),
)
writer, err := options.MakeWriter(app.Outputs, app.File, app.OutputTemplatePath)
if err != nil {
return fmt.Errorf("unable to write to report destination: %w", err)
}
if app.Attest.KeyRef != "" {
passFunc, err := selectPassFunc(app.Attest.KeyRef, app.Attest.Password)
if err != nil {
return err
defer func() {
if err := writer.Close(); err != nil {
fmt.Printf("unable to close report destination: %+v", err)
}
}()
ko.PassFunc = passFunc
}
sv, err := sign.SignerFromKeyOpts(ctx, "", "", ko)
// could be an image or a directory, with or without a scheme
// TODO: validate that source is image
userInput := args[0]
si, err := source.ParseInputWithName(userInput, app.Platform, true, app.Name)
if err != nil {
return err
return fmt.Errorf("could not generate source input for packages command: %w", err)
}
if si.Scheme != source.ImageScheme {
return fmt.Errorf("attestations are only supported for oci images at this time")
}
defer sv.Close()
eventBus := partybus.NewBus()
stereoscope.SetBus(eventBus)
@@ -107,7 +62,7 @@ func Run(ctx context.Context, app *config.Application, ko sigopts.KeyOpts, args
subscription := eventBus.Subscribe()
return eventloop.EventLoop(
execWorker(app, *si, format, predicateType, sv),
execWorker(app, *si, writer),
eventloop.SetupSignals(),
subscription,
stereoscope.Cleanup,
@@ -115,284 +70,158 @@ func Run(ctx context.Context, app *config.Application, ko sigopts.KeyOpts, args
)
}
func parseAttestationOutput(outputs []string) (format string) {
if len(outputs) == 0 {
outputs = append(outputs, string(syftjson.ID))
func buildSBOM(app *config.Application, si source.Input, writer sbom.Writer, errs chan error) ([]byte, error) {
src, cleanup, err := source.New(si, app.Registry.ToOptions(), app.Exclusions)
if cleanup != nil {
defer cleanup()
}
return outputs[0]
}
func parseImageSource(userInput string, app *config.Application) (s *source.Input, err error) {
si, err := source.ParseInputWithName(userInput, app.Platform, false, app.Name)
if err != nil {
return nil, fmt.Errorf("could not generate source input for attest command: %w", err)
return nil, fmt.Errorf("failed to construct source from user input %q: %w", si.UserInput, err)
}
switch si.Scheme {
case source.ImageScheme, source.UnknownScheme:
// at this point we know that it cannot be dir: or file: schemes;
// we will assume that the unknown scheme could represent an image;
si.Scheme = source.ImageScheme
default:
return nil, fmt.Errorf("attest command can only be used with image sources but discovered %q when given %q", si.Scheme, userInput)
s, err := packages.GenerateSBOM(src, errs, app)
if err != nil {
return nil, err
}
// if the original detection was from the local daemon we want to short circuit
// that and attempt to generate the image source from its current registry source instead
switch si.ImageSource {
case image.UnknownSource, image.OciRegistrySource:
si.ImageSource = image.OciRegistrySource
case image.SingularitySource:
default:
return nil, fmt.Errorf("attest command can only be used with image sources fetch directly from the registry, but discovered an image source of %q when given %q", si.ImageSource, userInput)
if s == nil {
return nil, fmt.Errorf("no SBOM produced for %q", si.UserInput)
}
return si, nil
// note: only works for single format no multi writer support
sBytes, err := writer.Bytes(*s)
if err != nil {
return nil, fmt.Errorf("unable to build SBOM bytes: %w", err)
}
return sBytes, nil
}
func execWorker(app *config.Application, sourceInput source.Input, format sbom.Format, predicateType string, sv *sign.SignerVerifier) <-chan error {
func execWorker(app *config.Application, si source.Input, writer sbom.Writer) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
src, cleanup, err := source.NewFromRegistry(sourceInput, app.Registry.ToOptions(), app.Exclusions)
if cleanup != nil {
defer cleanup()
}
sBytes, err := buildSBOM(app, si, writer, errs)
if err != nil {
errs <- fmt.Errorf("failed to construct source from user input %q: %w", sourceInput.UserInput, err)
errs <- fmt.Errorf("unable to build SBOM: %w", err)
return
}
s, err := packages.GenerateSBOM(src, errs, app)
if err != nil {
errs <- err
return
}
// TODO: add multi writer support
for _, o := range app.Outputs {
f, err := os.CreateTemp("", o)
if err != nil {
errs <- fmt.Errorf("unable to create temp file: %w", err)
return
}
sbomBytes, err := syft.Encode(*s, format)
if err != nil {
errs <- err
return
}
defer f.Close()
defer os.Remove(f.Name())
signedPayload, err := generateAttestation(sourceInput, sbomBytes, src, sv, predicateType)
if err != nil {
errs <- err
return
}
if _, err := f.Write(sBytes); err != nil {
errs <- fmt.Errorf("unable to write SBOM to temp file: %w", err)
return
}
err = publishAttestation(app, signedPayload, src, sv)
if err != nil {
errs <- err
return
// TODO: what other validation here besides binary name?
cmd := "cosign"
if !commandExists(cmd) {
errs <- fmt.Errorf("unable to find cosign in PATH; make sure you have it installed")
return
}
args := []string{"attest", si.UserInput, "--type", "custom", "--predicate", f.Name()}
execCmd := exec.Command(cmd, args...)
execCmd.Env = os.Environ()
execCmd.Env = append(execCmd.Env, "COSIGN_EXPERIMENTAL=1")
// bus adapter for ui to hook into stdout via an os pipe
r, w, err := os.Pipe()
if err != nil {
errs <- fmt.Errorf("unable to create os pipe: %w", err)
return
}
defer w.Close()
b := &busWriter{r: r, w: w, mon: &progress.Manual{N: -1}}
execCmd.Stdout = b
execCmd.Stderr = b
defer b.mon.SetCompleted()
// attest the SBOM
err = execCmd.Run()
if err != nil {
b.mon.Err = err
errs <- fmt.Errorf("unable to attest SBOM: %w", err)
return
}
}
bus.Publish(partybus.Event{
Type: event.Exit,
Value: func() error {
return nil
},
Type: event.Exit,
Value: func() error { return nil },
})
}()
return errs
}
func generateAttestation(si source.Input, predicate []byte, src *source.Source, sv *sign.SignerVerifier, predicateType string) ([]byte, error) {
var h v1.Hash
switch si.ImageSource {
case image.OciRegistrySource:
switch len(src.Image.Metadata.RepoDigests) {
case 0:
return nil, fmt.Errorf("cannot generate attestation since no repo digests were found; make sure you're passing an OCI registry source for the attest command")
case 1:
d, err := name.NewDigest(src.Image.Metadata.RepoDigests[0])
if err != nil {
return nil, err
}
h, err = v1.NewHash(d.Identifier())
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("cannot generate attestation since multiple repo digests were found for the image: %+v", src.Image.Metadata.RepoDigests)
}
case image.SingularitySource:
var err error
h, err = v1.NewHash(src.Image.Metadata.ID)
if err != nil {
return nil, err
func ValidateOutputOptions(app *config.Application) error {
var usesTemplateOutput bool
for _, o := range app.Outputs {
if o == template.ID.String() {
usesTemplateOutput = true
break
}
}
sh, err := attestation.GenerateStatement(attestation.GenerateOpts{
Predicate: bytes.NewBuffer(predicate),
Type: predicateType,
Digest: h.Hex,
})
if err != nil {
return nil, err
if usesTemplateOutput && app.OutputTemplatePath == "" {
return fmt.Errorf(`must specify path to template file when using "template" output format`)
}
payload, err := json.Marshal(sh)
if err != nil {
return nil, err
if len(app.Outputs) > 1 {
return fmt.Errorf("multiple SBOM format is not supported for attest at this time")
}
wrapped := dsse.WrapSigner(sv, intotoJSONDsseType)
return wrapped.SignMessage(bytes.NewReader(payload), signatureoptions.WithContext(context.Background()))
}
// publishAttestation publishes signedPayload to the location specified by the user.
func publishAttestation(app *config.Application, signedPayload []byte, src *source.Source, sv *sign.SignerVerifier) error {
switch {
// We want to give the option to not upload the generated attestation
// if passed or if the user is using local PKI
case app.Attest.NoUpload || app.Attest.KeyRef != "":
if app.File != "" {
return os.WriteFile(app.File, signedPayload, 0600)
}
_, err := os.Stdout.Write(signedPayload)
return err
default:
ref, err := name.ParseReference(src.Metadata.ImageMetadata.UserInput)
if err != nil {
return err
}
digest, err := ociremote.ResolveDigest(ref)
if err != nil {
return err
}
return uploadAttestation(app, signedPayload, digest, sv)
// cannot use table as default output format when using template output
if slices.Contains(app.Outputs, table.ID.String()) {
app.Outputs = []string{syftjson.ID.String()}
}
}
func trackUploadAttestation() (*progress.Stage, *progress.Manual) {
stage := &progress.Stage{}
prog := &progress.Manual{}
bus.Publish(partybus.Event{
Type: event.UploadAttestation,
Value: progress.StagedProgressable(&struct {
progress.Stager
progress.Progressable
}{
Stager: stage,
Progressable: prog,
}),
})
return stage, prog
}
// uploads signed SBOM payload to Rekor transparency log along with key information;
// returns a bundle for attestation annotations
// rekor bundle includes a signed payload and rekor timestamp;
// the bundle is then wrapped onto an OCI signed entity and uploaded to
// the user's image's OCI registry repository as *.att
func uploadAttestation(app *config.Application, signedPayload []byte, digest name.Digest, sv *sign.SignerVerifier) error {
// add application/vnd.dsse.envelope.v1+json as media type for other applications to decode attestation
opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)}
if sv.Cert != nil {
opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain))
}
stage, prog := trackUploadAttestation()
defer prog.SetCompleted() // just in case we return early
prog.Total = 2
stage.Current = "uploading signing information to transparency log"
// uploads payload to Rekor transparency log along with key information;
// returns bundle for attesation annotations
// rekor bundle includes a signed payload and rekor timestamp;
// the bundle is then wrapped onto an OCI signed entity and uploaded to
// the user's image's OCI registry repository as *.att
bundle, err := uploadToTlog(context.TODO(), sv, app.Attest.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) {
return cosign.TLogUploadInTotoAttestation(context.TODO(), r, signedPayload, b)
})
if err != nil {
return err
}
prog.N = 1
stage.Current = "uploading attestation to OCI registry"
// add bundle OCI attestation that is uploaded to
opts = append(opts, static.WithBundle(bundle))
sig, err := static.NewAttestation(signedPayload, opts...)
if err != nil {
return err
}
se, err := ociremote.SignedEntity(digest)
if err != nil {
return err
}
newSE, err := mutate.AttachAttestationToEntity(se, sig)
if err != nil {
return err
}
// Publish the attestations associated with this entity
err = ociremote.WriteAttestations(digest.Repository, newSE)
if err != nil {
return err
}
prog.SetCompleted()
return nil
}
func formatPredicateType(format sbom.Format) string {
switch format.ID() {
case spdxjson.ID:
return in_toto.PredicateSPDX
case cyclonedxjson.ID:
return in_toto.PredicateCycloneDX
case syftjson.ID:
return "https://syft.dev/bom"
default:
return ""
}
type busWriter struct {
w *os.File
r *os.File
hasWritten bool
mon *progress.Manual
}
type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error)
func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*cbundle.RekorBundle, error) {
var rekorBytes []byte
// Upload the cert or the public key, depending on what we have
if sv.Cert != nil {
rekorBytes = sv.Cert
} else {
pemBytes, err := sigs.PublicKeyPem(sv, signatureoptions.WithContext(ctx))
if err != nil {
return nil, err
}
rekorBytes = pemBytes
func (b *busWriter) Write(p []byte) (n int, err error) {
if !b.hasWritten {
b.hasWritten = true
bus.Publish(
partybus.Event{
Type: event.AttestationStarted,
Source: monitor.GenericTask{
Title: monitor.Title{
Default: "Create attestation",
WhileRunning: "Creating attestation",
OnSuccess: "Created attestation",
},
Context: "cosign",
},
Value: &monitor.ShellProgress{
Reader: b.r,
Manual: b.mon,
},
},
)
}
return b.w.Write(p)
}
rekorClient, err := rekor.NewClient(rekorURL)
if err != nil {
return nil, err
}
entry, err := upload(rekorClient, rekorBytes)
if err != nil {
return nil, err
}
if entry.LogIndex != nil {
log.Debugf("transparency log entry created with index: %v", *entry.LogIndex)
}
return cbundle.EntryToBundle(entry), nil
func commandExists(cmd string) bool {
_, err := exec.LookPath(cmd)
return err == nil
}
-57
View File
@@ -1,57 +0,0 @@
package attest
import (
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
)
func selectPassFunc(keypath, password string) (cosign.PassFunc, error) {
keyContents, err := os.ReadFile(keypath)
if err != nil {
return nil, err
}
var fn cosign.PassFunc = func(bool) (b []byte, err error) { return nil, nil }
_, err = cosign.LoadPrivateKey(keyContents, nil)
if err != nil {
fn = func(bool) (b []byte, err error) {
return fetchPassword(password)
}
}
return fn, nil
}
func fetchPassword(password string) (b []byte, err error) {
potentiallyPipedInput, err := internal.IsPipedInput()
if err != nil {
log.Warnf("unable to determine if there is piped input: %+v", err)
}
switch {
case password != "":
return []byte(password), nil
case potentiallyPipedInput:
// handle piped in passwords
pwBytes, err := io.ReadAll(os.Stdin)
if err != nil {
return nil, fmt.Errorf("unable to get password from stdin: %w", err)
}
// be resilient to input that may have newline characters (in case someone is using echo without -n)
cleanPw := strings.TrimRight(string(pwBytes), "\n")
return []byte(cleanPw), nil
case internal.IsTerminal():
return cosign.GetPassFromTerm(false)
}
return nil, errors.New("no method available to fetch password")
}
+8 -12
View File
@@ -26,8 +26,10 @@ import (
const indent = " "
// New constructs the `syft packages` command, aliases the root command to `syft packages`,
// and constructs the `syft power-user` and `syft attest` commands. It is also responsible for
// and constructs the `syft power-user` command. It is also responsible for
// organizing flag usage and injecting the application config for each command.
// It also constructs the syft attest command and the syft version command.
// Because of how the `cobra` library behaves, the application's configuration is initialized
// at this level. Values from the config should only be used after `app.LoadAllValues` has been called.
// Cobra does not have knowledge of the user provided flags until the `RunE` block of each command.
@@ -46,9 +48,9 @@ func New() (*cobra.Command, error) {
packagesCmd := Packages(v, app, ro, po)
// root options are also passed to the attestCmd so that a user provided config location can be discovered
attestCmd := Attest(v, app, ro)
poweruserCmd := PowerUser(v, app, ro)
convertCmd := Convert(v, app, ro, po)
attestCmd := Attest(v, app, ro, po)
// rootCmd is currently an alias for the packages command
rootCmd := &cobra.Command{
@@ -73,11 +75,7 @@ func New() (*cobra.Command, error) {
if err != nil {
return nil, err
}
// attest also uses flags from the packagesCmd since it generates an sbom
err = po.AddFlags(attestCmd, v)
if err != nil {
return nil, err
}
// poweruser also uses the packagesCmd flags since it is a specialized version of the command
err = po.AddFlags(poweruserCmd, v)
if err != nil {
@@ -87,13 +85,11 @@ func New() (*cobra.Command, error) {
// commands to add to root
cmds := []*cobra.Command{
packagesCmd,
attestCmd,
poweruserCmd,
convertCmd,
poweruserCmd,
poweruserCmd,
Completion(),
attestCmd,
Version(v, app),
cranecmd.NewCmdAuthLogin("syft"),
cranecmd.NewCmdAuthLogin("syft"), // syft login uses the same command as crane
}
// Add sub-commands.
-54
View File
@@ -1,54 +0,0 @@
package cli
import (
"os"
"github.com/spf13/cobra"
)
func Completion() *cobra.Command {
cmd := &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.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
RunE: func(cmd *cobra.Command, args []string) error {
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 {
return err
}
return nil
},
}
return cmd
}
-29
View File
@@ -22,7 +22,6 @@ func Tasks(app *config.Application) ([]Task, error) {
generateCatalogFileMetadataTask,
generateCatalogFileDigestsTask,
generateCatalogSecretsTask,
generateCatalogFileClassificationsTask,
generateCatalogContentsTask,
}
@@ -162,34 +161,6 @@ func generateCatalogSecretsTask(app *config.Application) (Task, error) {
return task, nil
}
func generateCatalogFileClassificationsTask(app *config.Application) (Task, error) {
if !app.FileClassification.Cataloger.Enabled {
return nil, nil
}
// TODO: in the future we could expose out the classifiers via configuration
classifierCataloger, err := file.NewClassificationCataloger(file.DefaultClassifiers)
if err != nil {
return nil, err
}
task := func(results *sbom.Artifacts, src *source.Source) ([]artifact.Relationship, error) {
resolver, err := src.FileResolver(app.FileClassification.Cataloger.ScopeOpt)
if err != nil {
return nil, err
}
result, err := classifierCataloger.Catalog(resolver)
if err != nil {
return nil, err
}
results.FileClassifications = result
return nil, nil
}
return task, nil
}
func generateCatalogContentsTask(app *config.Application) (Task, error) {
if !app.FileContents.Cataloger.Enabled {
return nil, nil
-77
View File
@@ -1,77 +0,0 @@
package options
import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
)
const defaultKeyFileName = "cosign.key"
type AttestOptions struct {
Key string
Cert string
CertChain string
NoUpload bool
Force bool
Recursive bool
Rekor RekorOptions
Fulcio FulcioOptions
OIDC OIDCOptions
}
var _ Interface = (*AttestOptions)(nil)
func (o *AttestOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
if err := o.Rekor.AddFlags(cmd, v); err != nil {
return err
}
if err := o.Fulcio.AddFlags(cmd, v); err != nil {
return err
}
if err := o.OIDC.AddFlags(cmd, v); err != nil {
return err
}
cmd.Flags().StringVarP(&o.Key, "key", "", defaultKeyFileName,
"path to the private key file to use for attestation")
cmd.Flags().StringVarP(&o.Cert, "cert", "", "",
"path to the x.509 certificate in PEM format to include in the OCI Signature")
cmd.Flags().BoolVarP(&o.NoUpload, "no-upload", "", false,
"do not upload the generated attestation")
cmd.Flags().BoolVarP(&o.Force, "force", "", false,
"skip warnings and confirmations")
cmd.Flags().BoolVarP(&o.Recursive, "recursive", "", false,
"if a multi-arch image is specified, additionally sign each discrete image")
return bindAttestationConfigOptions(cmd.Flags(), v)
}
func bindAttestationConfigOptions(flags *pflag.FlagSet, v *viper.Viper) error {
if err := v.BindPFlag("attest.key", flags.Lookup("key")); err != nil {
return err
}
if err := v.BindPFlag("attest.cert", flags.Lookup("cert")); err != nil {
return err
}
if err := v.BindPFlag("attest.no-upload", flags.Lookup("no-upload")); err != nil {
return err
}
if err := v.BindPFlag("attest.force", flags.Lookup("force")); err != nil {
return err
}
if err := v.BindPFlag("attest.recursive", flags.Lookup("recursive")); err != nil {
return err
}
return nil
}
+1 -1
View File
@@ -9,7 +9,7 @@ type VersionOptions struct {
Output string
}
var _ Interface = (*PackagesOptions)(nil)
var _ Interface = (*VersionOptions)(nil)
func (o *VersionOptions) AddFlags(cmd *cobra.Command, v *viper.Viper) error {
cmd.Flags().StringVarP(&o.Output, "output", "o", "text", "format to show version information (available=[text, json])")
+2 -2
View File
@@ -24,7 +24,7 @@ import (
)
func Run(ctx context.Context, app *config.Application, args []string) error {
err := validateOutputOptions(app)
err := ValidateOutputOptions(app)
if err != nil {
return err
}
@@ -134,7 +134,7 @@ func MergeRelationships(cs ...<-chan artifact.Relationship) (relationships []art
return relationships
}
func validateOutputOptions(app *config.Application) error {
func ValidateOutputOptions(app *config.Application) error {
var usesTemplateOutput bool
for _, o := range app.Outputs {
if o == template.ID.String() {
+9 -192
View File
@@ -3,14 +3,13 @@ module github.com/anchore/syft
go 1.18
require (
github.com/CycloneDX/cyclonedx-go v0.5.2
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/acobaugh/osrelease v0.1.0
github.com/adrg/xdg v0.3.3
github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/packageurl-go v0.1.1-0.20220428202044-a072fa3cb6d7
github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501
github.com/bmatcuk/doublestar/v4 v4.0.2
github.com/dustin/go-humanize v1.0.0
github.com/facebookincubator/nvdtools v0.1.4
@@ -37,7 +36,7 @@ require (
github.com/spf13/cobra v1.6.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.13.0
github.com/stretchr/testify v1.8.0
github.com/stretchr/testify v1.8.1
github.com/vifraa/gopom v0.1.0
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
@@ -50,268 +49,90 @@ require (
)
require (
github.com/CycloneDX/cyclonedx-go v0.7.1-0.20221222100750-41a1ac565cce
github.com/Masterminds/sprig/v3 v3.2.2
github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8
github.com/anchore/stereoscope v0.0.0-20221006201143-d24c9d626b33
github.com/anchore/stereoscope v0.0.0-20221208011002-c5ff155d72f1
github.com/docker/docker v20.10.17+incompatible
github.com/google/go-containerregistry v0.11.0
github.com/in-toto/in-toto-golang v0.4.1-0.20221018183522-731d0640b65f
github.com/invopop/jsonschema v0.7.0
github.com/knqyf263/go-rpmdb v0.0.0-20221030135625-4082a22221ce
github.com/opencontainers/go-digest v1.0.0
github.com/sassoftware/go-rpmutils v0.2.0
github.com/sigstore/cosign v1.13.1
github.com/sigstore/rekor v0.12.1-0.20220915152154-4bb6f441c1b2
github.com/sigstore/sigstore v1.4.4
github.com/vbatts/go-mtree v0.5.0
golang.org/x/exp v0.0.0-20220823124025-807a23277127
gopkg.in/yaml.v3 v3.0.1
)
require (
bitbucket.org/creachadair/shell v0.0.7 // indirect
cloud.google.com/go/compute v1.10.0 // indirect
github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0 // indirect
github.com/Azure/azure-sdk-for-go v66.0.0+incompatible // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.28 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/DataDog/zstd v1.4.5 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
github.com/alibabacloud-go/cr-20160607 v1.0.1 // indirect
github.com/alibabacloud-go/cr-20181201 v1.0.10 // indirect
github.com/alibabacloud-go/darabonba-openapi v0.1.18 // indirect
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect
github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect
github.com/alibabacloud-go/openapi-util v0.0.11 // indirect
github.com/alibabacloud-go/tea v1.1.18 // indirect
github.com/alibabacloud-go/tea-utils v1.4.4 // indirect
github.com/alibabacloud-go/tea-xml v1.1.2 // indirect
github.com/aliyun/credentials-go v1.2.3 // indirect
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
github.com/aws/aws-sdk-go-v2 v1.16.16 // indirect
github.com/aws/aws-sdk-go-v2/config v1.17.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.12.21 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.17 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.24 // indirect
github.com/aws/aws-sdk-go-v2/service/ecr v1.15.0 // indirect
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.12.0 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.17 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.11.23 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.16.19 // indirect
github.com/aws/smithy-go v1.13.3 // indirect
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220517224237-e6f29200ae04 // indirect
github.com/benbjohnson/clock v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chrismellard/docker-credential-acr-env v0.0.0-20220119192733-fe33c00cee21 // indirect
github.com/clbanning/mxj/v2 v2.5.6 // indirect
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect
github.com/containerd/containerd v1.6.8 // indirect
github.com/containerd/containerd v1.6.12 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.12.0 // indirect
github.com/coreos/go-oidc/v3 v3.4.0 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/docker/cli v20.10.17+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fullstorydev/grpcurl v1.8.7 // indirect
github.com/gabriel-vasile/mimetype v1.4.0 // indirect
github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.21.4 // indirect
github.com/go-openapi/errors v0.20.3 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/loads v0.21.2 // indirect
github.com/go-openapi/runtime v0.24.2 // indirect
github.com/go-openapi/spec v0.20.7 // indirect
github.com/go-openapi/strfmt v0.21.3 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-openapi/validate v0.22.0 // indirect
github.com/go-piv/piv-go v1.10.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.11.0 // indirect
github.com/go-restruct/restruct v1.2.0-alpha // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/certificate-transparency-go v1.1.3 // indirect
github.com/google/go-github/v45 v45.2.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/trillian v1.5.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
github.com/googleapis/gnostic v0.5.5 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect
github.com/jhump/protoreflect v1.13.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.3.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/klauspost/compress v1.15.9 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/letsencrypt/boulder v0.0.0-20220929215747-76583552c2be // indirect
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/miekg/pkcs11 v1.1.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mozillazg/docker-credential-acr-helper v0.3.0 // indirect
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
github.com/nwaples/rardecode v1.1.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.13.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 // indirect
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
github.com/segmentio/ksuid v1.0.4 // indirect
github.com/shibumi/go-pathspec v1.3.0 // indirect
github.com/rogpeppe/go-internal v1.8.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/sigstore/fulcio v0.6.0 // indirect
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
github.com/soheilhy/cmux v0.1.5 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.1.1 // indirect
github.com/stretchr/objx v0.4.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/sylabs/sif/v2 v2.8.1 // indirect
github.com/sylabs/squashfs v0.6.1 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 // indirect
github.com/thales-e-security/pool v0.0.2 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/theupdateframework/go-tuf v0.5.2-0.20220930112810-3890c1e7ace4 // indirect
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
github.com/tjfoc/gmsm v1.3.2 // indirect
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
github.com/transparency-dev/merkle v0.0.1 // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/urfave/cli v1.22.7 // indirect
github.com/vbatts/tar-split v0.11.2 // indirect
github.com/xanzy/go-gitlab v0.73.1 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
github.com/zeebo/errs v1.2.2 // indirect
go.etcd.io/bbolt v1.3.6 // indirect
go.etcd.io/etcd/api/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/client/v2 v2.306.0-alpha.0 // indirect
go.etcd.io/etcd/client/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/etcdctl/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/etcdutl/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/raft/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/server/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/tests/v3 v3.6.0-alpha.0 // indirect
go.etcd.io/etcd/v3 v3.6.0-alpha.0 // indirect
go.mongodb.org/mongo-driver v1.10.0 // indirect
go.opencensus.io v0.23.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 // indirect
go.opentelemetry.io/otel v1.7.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.7.0 // indirect
go.opentelemetry.io/otel/sdk v1.7.0 // indirect
go.opentelemetry.io/otel/trace v1.7.0 // indirect
go.opentelemetry.io/proto/otlp v0.16.0 // indirect
go.uber.org/atomic v1.10.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.23.0 // indirect
golang.org/x/exp v0.0.0-20220823124025-807a23277127 // indirect
golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1 // indirect
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 // indirect
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af // indirect
golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/api v0.99.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e // indirect
google.golang.org/grpc v1.50.1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
k8s.io/api v0.23.5 // indirect
k8s.io/apimachinery v0.23.5 // indirect
k8s.io/client-go v0.23.5 // indirect
k8s.io/klog/v2 v2.60.1-0.20220317184644-43cc75f9ae89 // indirect
k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
lukechampine.com/uint128 v1.1.1 // indirect
modernc.org/cc/v3 v3.36.0 // indirect
modernc.org/ccgo/v3 v3.16.6 // indirect
@@ -322,10 +143,6 @@ require (
modernc.org/sqlite v1.17.3 // indirect
modernc.org/strutil v1.1.1 // indirect
modernc.org/token v1.0.0 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/release-utils v0.7.3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
require (
+26 -982
View File
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -53,9 +53,9 @@ type Application struct {
Secrets secrets `yaml:"secrets" json:"secrets" mapstructure:"secrets"`
Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"`
Exclusions []string `yaml:"exclude" json:"exclude" mapstructure:"exclude"`
Attest attest `yaml:"attest" json:"attest" mapstructure:"attest"`
Platform string `yaml:"platform" json:"platform" mapstructure:"platform"`
Name string `yaml:"name" json:"name" mapstructure:"name"`
Parallelism int `yaml:"parallelism" json:"parallelism" mapstructure:"parallelism"` // the number of catalog workers to run in parallel
}
func (cfg Application) ToCatalogerConfig() cataloger.Config {
@@ -65,7 +65,8 @@ func (cfg Application) ToCatalogerConfig() cataloger.Config {
IncludeUnindexedArchives: cfg.Package.SearchUnindexedArchives,
Scope: cfg.Package.Cataloger.ScopeOpt,
},
Catalogers: cfg.Catalogers,
Catalogers: cfg.Catalogers,
Parallelism: cfg.Parallelism,
}
}
@@ -182,6 +183,7 @@ func loadDefaultValues(v *viper.Viper) {
v.SetDefault("quiet", false)
v.SetDefault("check-for-app-update", true)
v.SetDefault("catalogers", nil)
v.SetDefault("parallelism", 1)
// for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does
value := reflect.ValueOf(Application{})
-56
View File
@@ -1,56 +0,0 @@
package config
import (
"fmt"
"os"
"github.com/mitchellh/go-homedir"
"github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/spf13/viper"
)
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
type attest struct {
KeyRef string `yaml:"key" json:"key" mapstructure:"key"` // same as --key, file path to the private key
Cert string `yaml:"cert" json:"cert" mapstructure:"cert"`
NoUpload bool `yaml:"no_upload" json:"noUpload" mapstructure:"no_upload"`
Force bool `yaml:"force" json:"force" mapstructure:"force"`
Recursive bool `yaml:"recursive" json:"recursive" mapstructure:"recursive"`
Replace bool `yaml:"replace" json:"replace" mapstructure:"replace"`
Password string `yaml:"-" json:"-" mapstructure:"password"` // password for the private key
FulcioURL string `yaml:"fulcio_url" json:"fulcioUrl" mapstructure:"fulcio_url"`
FulcioIdentityToken string `yaml:"fulcio_identity_token" json:"fulcio_identity_token" mapstructure:"fulcio_identity_token"`
InsecureSkipFulcioVerify bool `yaml:"insecure_skip_verify" json:"insecure_skip_verify" mapstructure:"insecure_skip_verify"`
RekorURL string `yaml:"rekor_url" json:"rekorUrl" mapstructure:"rekor_url"`
OIDCIssuer string `yaml:"oidc_issuer" json:"oidcIssuer" mapstructure:"oidc_issuer"`
OIDCClientID string `yaml:"oidc_client_id" json:"oidcClientId" mapstructure:"oidc_client_id"`
OIDCRedirectURL string `yaml:"oidc_redirect_url" json:"OIDCRedirectURL" mapstructure:"oidc_redirect_url"`
}
func (cfg *attest) parseConfigValues() error {
if cfg.KeyRef != "" {
expandedPath, err := homedir.Expand(cfg.KeyRef)
if err != nil {
return fmt.Errorf("unable to expand key path=%q: %w", cfg.KeyRef, err)
}
cfg.KeyRef = expandedPath
}
if cfg.Password == "" {
// we allow for configuration via syft config/env vars and additionally interop with known cosign config env vars
if pw, ok := os.LookupEnv("COSIGN_PASSWORD"); ok {
cfg.Password = pw
}
}
return nil
}
func (cfg attest) loadDefaultValues(v *viper.Viper) {
v.SetDefault("attest.key", "")
v.SetDefault("attest.password", "")
v.SetDefault("attest.fulcio_url", options.DefaultFulcioURL)
v.SetDefault("attest.rekor_url", options.DefaultRekorURL)
v.SetDefault("attest.oidc_issuer", options.DefaultOIDCIssuerURL)
v.SetDefault("attest.oidc_client_id", "sigstore")
}
+1 -1
View File
@@ -6,5 +6,5 @@ const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "6.0.0"
JSONSchemaVersion = "6.1.0"
)
@@ -423,6 +423,7 @@ func Test_processSPDXLicense(t *testing.T) {
"cern-ohl-w-2": "CERN-OHL-W-2.0",
"cern-ohl-w-2.0": "CERN-OHL-W-2.0",
"cern-ohl-w-2.0.0": "CERN-OHL-W-2.0",
"checkmk": "checkmk",
"clartistic": "ClArtistic",
"cnri-jython": "CNRI-Jython",
"cnri-python": "CNRI-Python",
@@ -521,6 +522,7 @@ func Test_processSPDXLicense(t *testing.T) {
"fsfap": "FSFAP",
"fsful": "FSFUL",
"fsfullr": "FSFULLR",
"fsfullrwd": "FSFULLRWD",
"ftl": "FTL",
"gd": "GD",
"gfdl-1": "GFDL-1.1-only",
@@ -668,6 +670,7 @@ func Test_processSPDXLicense(t *testing.T) {
"jasper-2.0.0": "JasPer-2.0",
"jpnic": "JPNIC",
"json": "JSON",
"knuth-ctan": "Knuth-CTAN",
"lal-1": "LAL-1.2",
"lal-1.2": "LAL-1.2",
"lal-1.2.0": "LAL-1.2",
@@ -716,6 +719,7 @@ func Test_processSPDXLicense(t *testing.T) {
"libselinux-1.0": "libselinux-1.0",
"libselinux-1.0.0": "libselinux-1.0",
"libtiff": "libtiff",
"libutil-david-nugent": "libutil-David-Nugent",
"liliq-p-1": "LiLiQ-P-1.1",
"liliq-p-1.1": "LiLiQ-P-1.1",
"liliq-p-1.1.0": "LiLiQ-P-1.1",
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -1,9 +1,9 @@
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at 2022-10-14 11:46:16.097525 -0400 EDT m=+0.276930979
// This file was generated by robots at 2022-12-02 12:51:33.814385 -0500 EST m=+1.688731869
// using data from https://spdx.org/licenses/licenses.json
package spdxlicense
const Version = "3.18"
const Version = "3.19"
var licenseIDs = map[string]string{
"0bsd": "0BSD",
@@ -372,6 +372,7 @@ var licenseIDs = map[string]string{
"cern-ohl-w-2": "CERN-OHL-W-2.0",
"cern-ohl-w-2.0": "CERN-OHL-W-2.0",
"cern-ohl-w-2.0.0": "CERN-OHL-W-2.0",
"checkmk": "checkmk",
"clartistic": "ClArtistic",
"cnri-jython": "CNRI-Jython",
"cnri-python": "CNRI-Python",
@@ -470,6 +471,7 @@ var licenseIDs = map[string]string{
"fsfap": "FSFAP",
"fsful": "FSFUL",
"fsfullr": "FSFULLR",
"fsfullrwd": "FSFULLRWD",
"ftl": "FTL",
"gd": "GD",
"gfdl-1": "GFDL-1.1-only",
@@ -617,6 +619,7 @@ var licenseIDs = map[string]string{
"jasper-2.0.0": "JasPer-2.0",
"jpnic": "JPNIC",
"json": "JSON",
"knuth-ctan": "Knuth-CTAN",
"lal-1": "LAL-1.2",
"lal-1.2": "LAL-1.2",
"lal-1.2.0": "LAL-1.2",
@@ -665,6 +668,7 @@ var licenseIDs = map[string]string{
"libselinux-1.0": "libselinux-1.0",
"libselinux-1.0.0": "libselinux-1.0",
"libtiff": "libtiff",
"libutil-david-nugent": "libutil-David-Nugent",
"liliq-p-1": "LiLiQ-P-1.1",
"liliq-p-1.1": "LiLiQ-P-1.1",
"liliq-p-1.1.0": "LiLiQ-P-1.1",
@@ -984,7 +988,7 @@ var licenseIDs = map[string]string{
"sspl-1": "SSPL-1.0",
"sspl-1.0": "SSPL-1.0",
"sspl-1.0.0": "SSPL-1.0",
"standardml-nj": "StandardML-NJ",
"standardml-nj": "SMLNJ",
"sugarcrm-1": "SugarCRM-1.1.3",
"sugarcrm-1.1": "SugarCRM-1.1.3",
"sugarcrm-1.1.3": "SugarCRM-1.1.3",
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build linux || darwin
// +build linux darwin
//go:build linux || darwin || netbsd
// +build linux darwin netbsd
package ui
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build linux || darwin
// +build linux darwin
//go:build linux || darwin || netbsd
// +build linux darwin netbsd
package ui
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build linux || darwin
// +build linux darwin
//go:build linux || darwin || netbsd
// +build linux darwin netbsd
package ui
+27 -20
View File
@@ -27,26 +27,33 @@ can be extended to include specific package metadata struct shapes in the future
// This should represent all possible metadatas represented in the pkg.Package.Metadata field (an interface{}).
// When a new package metadata definition is created it will need to be manually added here. The variable name does
// not matter as long as it is exported.
// TODO: this should be generated from reflection of whats in the pkg package
type artifactMetadataContainer struct {
Apk pkg.ApkMetadata
Alpm pkg.AlpmMetadata
Dpkg pkg.DpkgMetadata
Gem pkg.GemMetadata
Java pkg.JavaMetadata
Npm pkg.NpmPackageJSONMetadata
Python pkg.PythonPackageMetadata
Rpm pkg.RpmMetadata
Cargo pkg.CargoPackageMetadata
Go pkg.GolangBinMetadata
Php pkg.PhpComposerJSONMetadata
Dart pkg.DartPubMetadata
Dotnet pkg.DotnetDepsMetadata
Portage pkg.PortageMetadata
Conan pkg.ConanMetadata
ConanLock pkg.ConanLockMetadata
KbPackage pkg.KbPackageMetadata
Hackage pkg.HackageMetadata
SwiftCocopods pkg.CocoapodsMetadata
Alpm pkg.AlpmMetadata
Apk pkg.ApkMetadata
Binary pkg.BinaryMetadata
Cocopods pkg.CocoapodsMetadata
Conan pkg.ConanMetadata
ConanLock pkg.ConanLockMetadata
Dart pkg.DartPubMetadata
Dotnet pkg.DotnetDepsMetadata
Dpkg pkg.DpkgMetadata
Gem pkg.GemMetadata
Go pkg.GolangMetadata
Hackage pkg.HackageMetadata
Java pkg.JavaMetadata
KbPackage pkg.KbPackageMetadata
NpmPackage pkg.NpmPackageJSONMetadata
NpmPackageLock pkg.NpmPackageLockJSONMetadata
MixLock pkg.MixLockMetadata
Php pkg.PhpComposerJSONMetadata
Portage pkg.PortageMetadata
PythonPackage pkg.PythonPackageMetadata
PythonPipfilelock pkg.PythonPipfileLockMetadata
Rebar pkg.RebarLockMetadata
Rpm pkg.RpmMetadata
RustCargo pkg.CargoPackageMetadata
}
func main() {
@@ -136,7 +143,7 @@ func write(schema []byte) {
}
// the generated schema is different, bail with error :(
fmt.Printf("Cowardly refusing to overwrite existing schema (%s)!\nSee the scheam/json/README.md for how to increment\n", filename)
fmt.Printf("Cowardly refusing to overwrite existing schema (%s)!\nSee the schema/json/README.md for how to increment\n", filename)
os.Exit(1)
}
File diff suppressed because it is too large Load Diff
-610
View File
@@ -1,610 +0,0 @@
{
"$schema" : "http://json-schema.org/draft-07/schema#",
"$id" : "http://spdx.org/rdf/terms",
"title" : "SPDX 2.2",
"type" : "object",
"properties" : {
"Document" : {
"type" : "object",
"properties" : {
"revieweds" : {
"description" : "Reviewed",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"reviewer" : {
"description" : "The name and, optionally, contact information of the person who performed the review. Values of this property must conform to the agent and tool syntax.",
"type" : "string"
},
"comment" : {
"type" : "string"
},
"reviewDate" : {
"description" : "The date and time at which the SpdxDocument was reviewed. This value must be in UTC and have 'Z' as its timezone indicator.",
"type" : "string"
}
}
}
},
"hasExtractedLicensingInfos" : {
"description" : "Indicates that a particular ExtractedLicensingInfo was defined in the subject SpdxDocument.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"seeAlsos" : {
"type" : "array",
"items" : {
"type" : "string"
}
},
"name" : {
"description" : "Identify name of this SpdxElement.",
"type" : "string"
},
"comment" : {
"type" : "string"
},
"licenseId" : {
"description" : "A human readable short form license identifier for a license. The license ID is iether on the standard license oist or the form \"LicenseRef-\"[idString] where [idString] is a unique string containing letters, numbers, \".\", \"-\" or \"+\".",
"type" : "string"
},
"extractedText" : {
"description" : "Verbatim license or licensing notice text that was discovered.",
"type" : "string"
}
},
"description" : "An ExtractedLicensingInfo represents a license or licensing notice that was found in the package. Any license text that is recognized as a license may be represented as a License rather than an ExtractedLicensingInfo."
}
},
"name" : {
"description" : "Identify name of this SpdxElement.",
"type" : "string"
},
"comment" : {
"type" : "string"
},
"spdxVersion" : {
"description" : "Provide a reference number that can be used to understand how to parse and interpret the rest of the file. It will enable both future changes to the specification and to support backward compatibility. The version number consists of a major and minor version indicator. The major field will be incremented when incompatible changes between versions are made (one or more sections are created, modified or deleted). The minor field will be incremented when backwards compatible changes are made.",
"type" : "string"
},
"annotations" : {
"description" : "Provide additional information about an SpdxElement.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"annotationDate" : {
"description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.",
"type" : "string"
},
"comment" : {
"type" : "string"
},
"annotator" : {
"description" : "This field identifies the person, organization or tool that has commented on a file, package, or the entire document.",
"type" : "string"
},
"annotationType" : {
"description" : "Type of the annotation.",
"type" : "string",
"enum" : [ "OTHER", "REVIEW" ]
}
},
"description" : "An Annotation is a comment on an SpdxItem by an agent."
}
},
"describesPackages" : {
"description" : "The describesPackage property relates an SpdxDocument to the package which it describes.",
"type" : "array",
"items" : {
"description" : "SPDX ID for Package. The describesPackage property relates an SpdxDocument to the package which it describes.",
"type" : "string"
}
},
"dataLicense" : {
"description" : "License expression for dataLicense. Compliance with the SPDX specification includes populating the SPDX fields therein with data related to such fields (\"SPDX-Metadata\"). The SPDX specification contains numerous fields where an SPDX document creator may provide relevant explanatory text in SPDX-Metadata. Without opining on the lawfulness of \"database rights\" (in jurisdictions where applicable), such explanatory text is copyrightable subject matter in most Berne Convention countries. By using the SPDX specification, or any portion hereof, you hereby agree that any copyright rights (as determined by your jurisdiction) in any SPDX-Metadata, including without limitation explanatory text, shall be subject to the terms of the Creative Commons CC0 1.0 Universal license. For SPDX-Metadata not containing any copyright rights, you hereby agree and acknowledge that the SPDX-Metadata is provided to you \"as-is\" and without any representations or warranties of any kind concerning the SPDX-Metadata, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non-infringement, or the absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.",
"type" : "string"
},
"externalDocumentRefs" : {
"description" : "Identify any external SPDX documents referenced within this SPDX document.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"externalDocumentId" : {
"description" : "externalDocumentId is a string containing letters, numbers, ., - and/or + which uniquely identifies an external document within this document.",
"type" : "string"
},
"checksum" : {
"type" : "object",
"properties" : {
"algorithm" : {
"description" : "Identifies the algorithm used to produce the subject Checksum. Currently, SHA-1 is the only supported algorithm. It is anticipated that other algorithms will be supported at a later time.",
"type" : "string",
"enum" : [ "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224" ]
},
"checksumValue" : {
"description" : "The checksumValue property provides a lower case hexidecimal encoded digest value produced using a specific algorithm.",
"type" : "string"
}
},
"description" : "A Checksum is value that allows the contents of a file to be authenticated. Even small changes to the content of the file will change its checksum. This class allows the results of a variety of checksum and cryptographic message digest algorithms to be represented."
},
"spdxDocument" : {
"description" : "SPDX ID for SpdxDocument. A propoerty containing an SPDX document.",
"type" : "string"
}
},
"description" : "Information about an external SPDX document reference including the checksum. This allows for verification of the external references."
}
},
"creationInfo" : {
"type" : "object",
"properties" : {
"comment" : {
"type" : "string"
},
"created" : {
"description" : "Identify when the SPDX file was originally created. The date is to be specified according to combined date and time in UTC format as specified in ISO 8601 standard. This field is distinct from the fields in section 8, which involves the addition of information during a subsequent review.",
"type" : "string"
},
"creators" : {
"description" : "Identify who (or what, in the case of a tool) created the SPDX file. If the SPDX file was created by an individual, indicate the person's name. If the SPDX file was created on behalf of a company or organization, indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person name or organization name may be designated as “anonymous” if appropriate.",
"type" : "array",
"items" : {
"description" : "Identify who (or what, in the case of a tool) created the SPDX file. If the SPDX file was created by an individual, indicate the person's name. If the SPDX file was created on behalf of a company or organization, indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person name or organization name may be designated as “anonymous” if appropriate.",
"type" : "string"
},
"minItems" : 1
},
"licenseListVersion" : {
"description" : "An optional field for creators of the SPDX file to provide the version of the SPDX License List used when the SPDX file was created.",
"type" : "string"
}
},
"description" : "One instance is required for each SPDX file produced. It provides the necessary information for forward and backward compatibility for processing tools."
},
"packages" : {
"description" : "Packages referenced in the SPDX document",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"attributionTexts" : {
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
"type" : "array",
"items" : {
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
"type" : "string"
}
},
"annotations" : {
"description" : "Provide additional information about an SpdxElement.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"annotationDate" : {
"description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.",
"type" : "string"
},
"comment" : {
"type" : "string"
},
"annotator" : {
"description" : "This field identifies the person, organization or tool that has commented on a file, package, or the entire document.",
"type" : "string"
},
"annotationType" : {
"description" : "Type of the annotation.",
"type" : "string",
"enum" : [ "OTHER", "REVIEW" ]
}
},
"description" : "An Annotation is a comment on an SpdxItem by an agent."
}
},
"supplier" : {
"description" : "The name and, optionally, contact information of the person or organization who was the immediate supplier of this package to the recipient. The supplier may be different than originator when the software has been repackaged. Values of this property must conform to the agent and tool syntax.",
"type" : "string"
},
"homepage" : {
"type" : "string"
},
"packageVerificationCode" : {
"type" : "object",
"properties" : {
"packageVerificationCodeValue" : {
"description" : "The actual package verification code as a hex encoded value.",
"type" : "string"
},
"packageVerificationCodeExcludedFiles" : {
"description" : "A file that was excluded when calculating the package verification code. This is usually a file containing SPDX data regarding the package. If a package contains more than one SPDX file all SPDX files must be excluded from the package verification code. If this is not done it would be impossible to correctly calculate the verification codes in both files.",
"type" : "array",
"items" : {
"description" : "A file that was excluded when calculating the package verification code. This is usually a file containing SPDX data regarding the package. If a package contains more than one SPDX file all SPDX files must be excluded from the package verification code. If this is not done it would be impossible to correctly calculate the verification codes in both files.",
"type" : "string"
}
}
},
"description" : "A manifest based verification code (the algorithm is defined in section 4.7 of the full specification) of the SPDX Item. This allows consumers of this data and/or database to determine if an SPDX item they have in hand is identical to the SPDX item from which the data was produced. This algorithm works even if the SPDX document is included in the SPDX item."
},
"checksums" : {
"description" : "The checksum property provides a mechanism that can be used to verify that the contents of a File or Package have not changed.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"algorithm" : {
"description" : "Identifies the algorithm used to produce the subject Checksum. Currently, SHA-1 is the only supported algorithm. It is anticipated that other algorithms will be supported at a later time.",
"type" : "string",
"enum" : [ "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224" ]
},
"checksumValue" : {
"description" : "The checksumValue property provides a lower case hexidecimal encoded digest value produced using a specific algorithm.",
"type" : "string"
}
},
"description" : "A Checksum is value that allows the contents of a file to be authenticated. Even small changes to the content of the file will change its checksum. This class allows the results of a variety of checksum and cryptographic message digest algorithms to be represented."
}
},
"downloadLocation" : {
"description" : "The URI at which this package is available for download. Private (i.e., not publicly reachable) URIs are acceptable as values of this property. The values http://spdx.org/rdf/terms#none and http://spdx.org/rdf/terms#noassertion may be used to specify that the package is not downloadable or that no attempt was made to determine its download location, respectively.",
"type" : "string"
},
"filesAnalyzed" : {
"description" : "Indicates whether the file content of this package has been available for or subjected to analysis when creating the SPDX document. If false indicates packages that represent metadata or URI references to a project, product, artifact, distribution or a component. If set to false, the package must not contain any files.",
"type" : "boolean"
},
"externalRefs" : {
"description" : "An External Reference allows a Package to reference an external source of additional information, metadata, enumerations, asset identifiers, or downloadable content believed to be relevant to the Package.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"comment" : {
"type" : "string"
},
"referenceCategory" : {
"description" : "Category for the external reference",
"type" : "string",
"enum" : [ "OTHER", "SECURITY", "PACKAGE_MANAGER" ]
},
"referenceLocator" : {
"description" : "The unique string with no spaces necessary to access the package-specific information, metadata, or content within the target location. The format of the locator is subject to constraints defined by the <type>.",
"type" : "string"
},
"referenceType" : {
"description" : "Type of the external reference. These are definined in an appendix in the SPDX specification.",
"type" : "string"
}
},
"description" : "An External Reference allows a Package to reference an external source of additional information, metadata, enumerations, asset identifiers, or downloadable content believed to be relevant to the Package."
}
},
"licenseComments" : {
"description" : "The licenseComments property allows the preparer of the SPDX document to describe why the licensing in spdx:licenseConcluded was chosen.",
"type" : "string"
},
"name" : {
"description" : "Identify name of this SpdxElement.",
"type" : "string"
},
"hasFiles" : {
"description" : "Indicates that a particular file belongs to a package.",
"type" : "array",
"items" : {
"description" : "SPDX ID for File. Indicates that a particular file belongs to a package.",
"type" : "string"
}
},
"comment" : {
"type" : "string"
},
"summary" : {
"description" : "Provides a short description of the package.",
"type" : "string"
},
"copyrightText" : {
"description" : "The text of copyright declarations recited in the Package or File.",
"type" : "string"
},
"originator" : {
"description" : "The name and, optionally, contact information of the person or organization that originally created the package. Values of this property must conform to the agent and tool syntax.",
"type" : "string"
},
"packageFileName" : {
"description" : "The base name of the package file name. For example, zlib-1.2.5.tar.gz.",
"type" : "string"
},
"licenseInfoFromFiles" : {
"description" : "The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.",
"type" : "array",
"items" : {
"description" : "License expression for licenseInfoFromFiles. The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.",
"type" : "string"
}
},
"versionInfo" : {
"description" : "Provides an indication of the version of the package that is described by this SpdxDocument.",
"type" : "string"
},
"sourceInfo" : {
"description" : "Allows the producer(s) of the SPDX document to describe how the package was acquired and/or changed from the original source.",
"type" : "string"
},
"description" : {
"description" : "Provides a detailed description of the package.",
"type" : "string"
}
}
}
},
"files" : {
"description" : "Files referenced in the SPDX document",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"fileTypes" : {
"description" : "The type of the file.",
"type" : "array",
"items" : {
"description" : "The type of the file.",
"type" : "string",
"enum" : [ "OTHER", "DOCUMENTATION", "IMAGE", "VIDEO", "ARCHIVE", "SPDX", "APPLICATION", "SOURCE", "BINARY", "TEXT", "AUDIO" ]
}
},
"attributionTexts" : {
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
"type" : "array",
"items" : {
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
"type" : "string"
}
},
"annotations" : {
"description" : "Provide additional information about an SpdxElement.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"annotationDate" : {
"description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.",
"type" : "string"
},
"comment" : {
"type" : "string"
},
"annotator" : {
"description" : "This field identifies the person, organization or tool that has commented on a file, package, or the entire document.",
"type" : "string"
},
"annotationType" : {
"description" : "Type of the annotation.",
"type" : "string",
"enum" : [ "OTHER", "REVIEW" ]
}
},
"description" : "An Annotation is a comment on an SpdxItem by an agent."
}
},
"checksums" : {
"description" : "The checksum property provides a mechanism that can be used to verify that the contents of a File or Package have not changed.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"algorithm" : {
"description" : "Identifies the algorithm used to produce the subject Checksum. Currently, SHA-1 is the only supported algorithm. It is anticipated that other algorithms will be supported at a later time.",
"type" : "string",
"enum" : [ "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224" ]
},
"checksumValue" : {
"description" : "The checksumValue property provides a lower case hexidecimal encoded digest value produced using a specific algorithm.",
"type" : "string"
}
},
"description" : "A Checksum is value that allows the contents of a file to be authenticated. Even small changes to the content of the file will change its checksum. This class allows the results of a variety of checksum and cryptographic message digest algorithms to be represented."
},
"minItems" : 1
},
"noticeText" : {
"description" : "This field provides a place for the SPDX file creator to record potential legal notices found in the file. This may or may not include copyright statements.",
"type" : "string"
},
"artifactOfs" : {
"description" : "Indicates the project in which the SpdxElement originated. Tools must preserve doap:homepage and doap:name properties and the URI (if one is known) of doap:Project resources that are values of this property. All other properties of doap:Projects are not directly supported by SPDX and may be dropped when translating to or from some SPDX formats.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : { }
}
},
"licenseComments" : {
"description" : "The licenseComments property allows the preparer of the SPDX document to describe why the licensing in spdx:licenseConcluded was chosen.",
"type" : "string"
},
"fileName" : {
"description" : "The name of the file relative to the root of the package.",
"type" : "string"
},
"name" : {
"description" : "Identify name of this SpdxElement.",
"type" : "string"
},
"comment" : {
"type" : "string"
},
"copyrightText" : {
"description" : "The text of copyright declarations recited in the Package or File.",
"type" : "string"
},
"fileContributors" : {
"description" : "This field provides a place for the SPDX file creator to record file contributors. Contributors could include names of copyright holders and/or authors who may not be copyright holders yet contributed to the file content.",
"type" : "array",
"items" : {
"description" : "This field provides a place for the SPDX file creator to record file contributors. Contributors could include names of copyright holders and/or authors who may not be copyright holders yet contributed to the file content.",
"type" : "string"
}
},
"licenseInfoInFiles" : {
"description" : "Licensing information that was discovered directly in the subject file. This is also considered a declared license for the file.",
"type" : "array",
"items" : {
"description" : "License expression for licenseInfoInFile. Licensing information that was discovered directly in the subject file. This is also considered a declared license for the file.",
"type" : "string"
},
"minItems" : 1
},
"licenseInfoFromFiles" : {
"description" : "The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.",
"type" : "array",
"items" : {
"description" : "License expression for licenseInfoFromFiles. The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.",
"type" : "string"
}
},
"fileDependencies" : {
"type" : "array",
"items" : {
"description" : "SPDX ID for File",
"type" : "string"
}
}
}
}
},
"snippets" : {
"description" : "Snippets referenced in the SPDX document",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"ranges" : {
"description" : "This field defines the byte range in the original host file (in X.2) that the snippet information applies to",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"startPointer" : {
"type" : "object",
"properties" : {
"reference" : {
"description" : "SPDX ID for File",
"type" : "string"
}
}
},
"endPointer" : {
"type" : "object",
"properties" : {
"reference" : {
"description" : "SPDX ID for File",
"type" : "string"
}
}
}
}
},
"minItems" : 1
},
"licenseComments" : {
"description" : "The licenseComments property allows the preparer of the SPDX document to describe why the licensing in spdx:licenseConcluded was chosen.",
"type" : "string"
},
"attributionTexts" : {
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
"type" : "array",
"items" : {
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
"type" : "string"
}
},
"name" : {
"description" : "Identify name of this SpdxElement.",
"type" : "string"
},
"snippetFromFile" : {
"description" : "SPDX ID for File. File containing the SPDX element (e.g. the file contaning a snippet).",
"type" : "string"
},
"comment" : {
"type" : "string"
},
"copyrightText" : {
"description" : "The text of copyright declarations recited in the Package or File.",
"type" : "string"
},
"licenseInfoInSnippets" : {
"description" : "Licensing information that was discovered directly in the subject snippet. This is also considered a declared license for the snippet.",
"type" : "array",
"items" : {
"description" : "License expression for licenseInfoInSnippet. Licensing information that was discovered directly in the subject snippet. This is also considered a declared license for the snippet.",
"type" : "string"
}
},
"annotations" : {
"description" : "Provide additional information about an SpdxElement.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"annotationDate" : {
"description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.",
"type" : "string"
},
"comment" : {
"type" : "string"
},
"annotator" : {
"description" : "This field identifies the person, organization or tool that has commented on a file, package, or the entire document.",
"type" : "string"
},
"annotationType" : {
"description" : "Type of the annotation.",
"type" : "string",
"enum" : [ "OTHER", "REVIEW" ]
}
},
"description" : "An Annotation is a comment on an SpdxItem by an agent."
}
},
"licenseInfoFromFiles" : {
"description" : "The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.",
"type" : "array",
"items" : {
"description" : "License expression for licenseInfoFromFiles. The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.",
"type" : "string"
}
}
}
}
},
"relationships" : {
"description" : "Relationships referenced in the SPDX document",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"comment" : {
"type" : "string"
},
"relationshipType" : {
"description" : "Describes the type of relationship between two SPDX elements.",
"type" : "string",
"enum" : [ "VARIANT_OF", "COPY_OF", "PATCH_FOR", "TEST_DEPENDENCY_OF", "CONTAINED_BY", "DATA_FILE_OF", "OPTIONAL_COMPONENT_OF", "ANCESTOR_OF", "GENERATES", "CONTAINS", "OPTIONAL_DEPENDENCY_OF", "FILE_ADDED", "DEV_DEPENDENCY_OF", "DEPENDENCY_OF", "BUILD_DEPENDENCY_OF", "DESCRIBES", "PREREQUISITE_FOR", "HAS_PREREQUISITE", "PROVIDED_DEPENDENCY_OF", "DYNAMIC_LINK", "DESCRIBED_BY", "METAFILE_OF", "DEPENDENCY_MANIFEST_OF", "PATCH_APPLIED", "RUNTIME_DEPENDENCY_OF", "TEST_OF", "TEST_TOOL_OF", "DEPENDS_ON", "FILE_MODIFIED", "DISTRIBUTION_ARTIFACT", "DOCUMENTATION_OF", "GENERATED_FROM", "STATIC_LINK", "OTHER", "BUILD_TOOL_OF", "TEST_CASE_OF", "PACKAGE_OF", "DESCENDANT_OF", "FILE_DELETED", "EXPANDED_FROM_ARCHIVE", "DEV_TOOL_OF", "EXAMPLE_OF" ]
},
"relatedSpdxElement" : {
"description" : "SPDX ID for SpdxElement. A related SpdxElement.",
"type" : "string"
}
}
}
}
}
}
}
}
+740
View File
@@ -0,0 +1,740 @@
{
"$schema" : "http://json-schema.org/draft-07/schema#",
"$id" : "http://spdx.org/rdf/terms/2.3",
"title" : "SPDX 2.3",
"type" : "object",
"properties" : {
"SPDXID" : {
"type" : "string",
"description" : "Uniquely identify any element in an SPDX document which may be referenced by other elements."
},
"annotations" : {
"description" : "Provide additional information about an SpdxElement.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"annotationDate" : {
"description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.",
"type" : "string"
},
"annotationType" : {
"description" : "Type of the annotation.",
"type" : "string",
"enum" : [ "OTHER", "REVIEW" ]
},
"annotator" : {
"description" : "This field identifies the person, organization, or tool that has commented on a file, package, snippet, or the entire document.",
"type" : "string"
},
"comment" : {
"type" : "string"
}
},
"required" : [ "annotationDate", "annotationType", "annotator", "comment" ],
"additionalProperties" : false,
"description" : "An Annotation is a comment on an SpdxItem by an agent."
}
},
"comment" : {
"type" : "string"
},
"creationInfo" : {
"type" : "object",
"properties" : {
"comment" : {
"type" : "string"
},
"created" : {
"description" : "Identify when the SPDX document was originally created. The date is to be specified according to combined date and time in UTC format as specified in ISO 8601 standard.",
"type" : "string"
},
"creators" : {
"description" : "Identify who (or what, in the case of a tool) created the SPDX document. If the SPDX document was created by an individual, indicate the person's name. If the SPDX document was created on behalf of a company or organization, indicate the entity name. If the SPDX document was created using a software tool, indicate the name and version for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person name or organization name may be designated as “anonymous” if appropriate.",
"minItems" : 1,
"type" : "array",
"items" : {
"description" : "Identify who (or what, in the case of a tool) created the SPDX document. If the SPDX document was created by an individual, indicate the person's name. If the SPDX document was created on behalf of a company or organization, indicate the entity name. If the SPDX document was created using a software tool, indicate the name and version for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person name or organization name may be designated as “anonymous” if appropriate.",
"type" : "string"
}
},
"licenseListVersion" : {
"description" : "An optional field for creators of the SPDX file to provide the version of the SPDX License List used when the SPDX file was created.",
"type" : "string"
}
},
"required" : [ "created", "creators" ],
"additionalProperties" : false,
"description" : "One instance is required for each SPDX file produced. It provides the necessary information for forward and backward compatibility for processing tools."
},
"dataLicense" : {
"description" : "License expression for dataLicense. See SPDX Annex D for the license expression syntax. Compliance with the SPDX specification includes populating the SPDX fields therein with data related to such fields (\"SPDX-Metadata\"). The SPDX specification contains numerous fields where an SPDX document creator may provide relevant explanatory text in SPDX-Metadata. Without opining on the lawfulness of \"database rights\" (in jurisdictions where applicable), such explanatory text is copyrightable subject matter in most Berne Convention countries. By using the SPDX specification, or any portion hereof, you hereby agree that any copyright rights (as determined by your jurisdiction) in any SPDX-Metadata, including without limitation explanatory text, shall be subject to the terms of the Creative Commons CC0 1.0 Universal license. For SPDX-Metadata not containing any copyright rights, you hereby agree and acknowledge that the SPDX-Metadata is provided to you \"as-is\" and without any representations or warranties of any kind concerning the SPDX-Metadata, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non-infringement, or the absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.",
"type" : "string"
},
"externalDocumentRefs" : {
"description" : "Identify any external SPDX documents referenced within this SPDX document.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"checksum" : {
"type" : "object",
"properties" : {
"algorithm" : {
"description" : "Identifies the algorithm used to produce the subject Checksum. Currently, SHA-1 is the only supported algorithm. It is anticipated that other algorithms will be supported at a later time.",
"type" : "string",
"enum" : [ "SHA1", "BLAKE3", "SHA3-384", "SHA256", "SHA384", "BLAKE2b-512", "BLAKE2b-256", "SHA3-512", "MD2", "ADLER32", "MD4", "SHA3-256", "BLAKE2b-384", "SHA512", "MD6", "MD5", "SHA224" ]
},
"checksumValue" : {
"description" : "The checksumValue property provides a lower case hexidecimal encoded digest value produced using a specific algorithm.",
"type" : "string"
}
},
"required" : [ "algorithm", "checksumValue" ],
"additionalProperties" : false,
"description" : "A Checksum is value that allows the contents of a file to be authenticated. Even small changes to the content of the file will change its checksum. This class allows the results of a variety of checksum and cryptographic message digest algorithms to be represented."
},
"externalDocumentId" : {
"description" : "externalDocumentId is a string containing letters, numbers, ., - and/or + which uniquely identifies an external document within this document.",
"type" : "string"
},
"spdxDocument" : {
"description" : "SPDX ID for SpdxDocument. A property containing an SPDX document.",
"type" : "string"
}
},
"required" : [ "checksum", "externalDocumentId", "spdxDocument" ],
"additionalProperties" : false,
"description" : "Information about an external SPDX document reference including the checksum. This allows for verification of the external references."
}
},
"hasExtractedLicensingInfos" : {
"description" : "Indicates that a particular ExtractedLicensingInfo was defined in the subject SpdxDocument.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"comment" : {
"type" : "string"
},
"crossRefs" : {
"description" : "Cross Reference Detail for a license SeeAlso URL",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"isLive" : {
"description" : "Indicate a URL is still a live accessible location on the public internet",
"type" : "boolean"
},
"isValid" : {
"description" : "True if the URL is a valid well formed URL",
"type" : "boolean"
},
"isWayBackLink" : {
"description" : "True if the License SeeAlso URL points to a Wayback archive",
"type" : "boolean"
},
"match" : {
"description" : "Status of a License List SeeAlso URL reference if it refers to a website that matches the license text.",
"type" : "string"
},
"order" : {
"description" : "The ordinal order of this element within a list",
"type" : "integer"
},
"timestamp" : {
"description" : "Timestamp",
"type" : "string"
},
"url" : {
"description" : "URL Reference",
"type" : "string"
}
},
"required" : [ "url" ],
"additionalProperties" : false,
"description" : "Cross reference details for the a URL reference"
}
},
"extractedText" : {
"description" : "Provide a copy of the actual text of the license reference extracted from the package, file or snippet that is associated with the License Identifier to aid in future analysis.",
"type" : "string"
},
"licenseId" : {
"description" : "A human readable short form license identifier for a license. The license ID is either on the standard license list or the form \"LicenseRef-[idString]\" where [idString] is a unique string containing letters, numbers, \".\" or \"-\". When used within a license expression, the license ID can optionally include a reference to an external document in the form \"DocumentRef-[docrefIdString]:LicenseRef-[idString]\" where docRefIdString is an ID for an external document reference.",
"type" : "string"
},
"name" : {
"description" : "Identify name of this SpdxElement.",
"type" : "string"
},
"seeAlsos" : {
"type" : "array",
"items" : {
"type" : "string"
}
}
},
"required" : [ "extractedText", "licenseId" ],
"additionalProperties" : false,
"description" : "An ExtractedLicensingInfo represents a license or licensing notice that was found in a package, file or snippet. Any license text that is recognized as a license may be represented as a License rather than an ExtractedLicensingInfo."
}
},
"name" : {
"description" : "Identify name of this SpdxElement.",
"type" : "string"
},
"revieweds" : {
"description" : "Reviewed",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"comment" : {
"type" : "string"
},
"reviewDate" : {
"description" : "The date and time at which the SpdxDocument was reviewed. This value must be in UTC and have 'Z' as its timezone indicator.",
"type" : "string"
},
"reviewer" : {
"description" : "The name and, optionally, contact information of the person who performed the review. Values of this property must conform to the agent and tool syntax. The reviewer property is deprecated in favor of Annotation with an annotationType review.",
"type" : "string"
}
},
"required" : [ "reviewDate" ],
"additionalProperties" : false,
"description" : "This class has been deprecated in favor of an Annotation with an Annotation type of review."
}
},
"spdxVersion" : {
"description" : "Provide a reference number that can be used to understand how to parse and interpret the rest of the file. It will enable both future changes to the specification and to support backward compatibility. The version number consists of a major and minor version indicator. The major field will be incremented when incompatible changes between versions are made (one or more sections are created, modified or deleted). The minor field will be incremented when backwards compatible changes are made.",
"type" : "string"
},
"documentNamespace" : {
"type" : "string",
"description" : "The URI provides an unambiguous mechanism for other SPDX documents to reference SPDX elements within this SPDX document."
},
"documentDescribes" : {
"description" : "Packages, files and/or Snippets described by this SPDX document",
"type" : "array",
"items" : {
"type" : "string",
"description" : "SPDX ID for each Package, File, or Snippet."
}
},
"packages" : {
"description" : "Packages referenced in the SPDX document",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"SPDXID" : {
"type" : "string",
"description" : "Uniquely identify any element in an SPDX document which may be referenced by other elements."
},
"annotations" : {
"description" : "Provide additional information about an SpdxElement.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"annotationDate" : {
"description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.",
"type" : "string"
},
"annotationType" : {
"description" : "Type of the annotation.",
"type" : "string",
"enum" : [ "OTHER", "REVIEW" ]
},
"annotator" : {
"description" : "This field identifies the person, organization, or tool that has commented on a file, package, snippet, or the entire document.",
"type" : "string"
},
"comment" : {
"type" : "string"
}
},
"required" : [ "annotationDate", "annotationType", "annotator", "comment" ],
"additionalProperties" : false,
"description" : "An Annotation is a comment on an SpdxItem by an agent."
}
},
"attributionTexts" : {
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include the actual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
"type" : "array",
"items" : {
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include the actual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
"type" : "string"
}
},
"builtDate" : {
"description" : "This field provides a place for recording the actual date the package was built.",
"type" : "string"
},
"checksums" : {
"description" : "The checksum property provides a mechanism that can be used to verify that the contents of a File or Package have not changed.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"algorithm" : {
"description" : "Identifies the algorithm used to produce the subject Checksum. Currently, SHA-1 is the only supported algorithm. It is anticipated that other algorithms will be supported at a later time.",
"type" : "string",
"enum" : [ "SHA1", "BLAKE3", "SHA3-384", "SHA256", "SHA384", "BLAKE2b-512", "BLAKE2b-256", "SHA3-512", "MD2", "ADLER32", "MD4", "SHA3-256", "BLAKE2b-384", "SHA512", "MD6", "MD5", "SHA224" ]
},
"checksumValue" : {
"description" : "The checksumValue property provides a lower case hexidecimal encoded digest value produced using a specific algorithm.",
"type" : "string"
}
},
"required" : [ "algorithm", "checksumValue" ],
"additionalProperties" : false,
"description" : "A Checksum is value that allows the contents of a file to be authenticated. Even small changes to the content of the file will change its checksum. This class allows the results of a variety of checksum and cryptographic message digest algorithms to be represented."
}
},
"comment" : {
"type" : "string"
},
"copyrightText" : {
"description" : "The text of copyright declarations recited in the package, file or snippet.\n\nIf the copyrightText field is not present, it implies an equivalent meaning to NOASSERTION.",
"type" : "string"
},
"description" : {
"description" : "Provides a detailed description of the package.",
"type" : "string"
},
"downloadLocation" : {
"description" : "The URI at which this package is available for download. Private (i.e., not publicly reachable) URIs are acceptable as values of this property. The values http://spdx.org/rdf/terms#none and http://spdx.org/rdf/terms#noassertion may be used to specify that the package is not downloadable or that no attempt was made to determine its download location, respectively.",
"type" : "string"
},
"externalRefs" : {
"description" : "An External Reference allows a Package to reference an external source of additional information, metadata, enumerations, asset identifiers, or downloadable content believed to be relevant to the Package.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"comment" : {
"type" : "string"
},
"referenceCategory" : {
"description" : "Category for the external reference",
"type" : "string",
"enum" : [ "OTHER", "PERSISTENT-ID", "PERSISTENT_ID", "SECURITY", "PACKAGE-MANAGER", "PACKAGE_MANAGER" ]
},
"referenceLocator" : {
"description" : "The unique string with no spaces necessary to access the package-specific information, metadata, or content within the target location. The format of the locator is subject to constraints defined by the <type>.",
"type" : "string"
},
"referenceType" : {
"description" : "Type of the external reference. These are defined in an appendix in the SPDX specification.",
"type" : "string"
}
},
"required" : [ "referenceCategory", "referenceLocator", "referenceType" ],
"additionalProperties" : false,
"description" : "An External Reference allows a Package to reference an external source of additional information, metadata, enumerations, asset identifiers, or downloadable content believed to be relevant to the Package."
}
},
"filesAnalyzed" : {
"description" : "Indicates whether the file content of this package has been available for or subjected to analysis when creating the SPDX document. If false indicates packages that represent metadata or URI references to a project, product, artifact, distribution or a component. If set to false, the package must not contain any files.",
"type" : "boolean"
},
"hasFiles" : {
"description" : "Indicates that a particular file belongs to a package.",
"type" : "array",
"items" : {
"description" : "SPDX ID for File. Indicates that a particular file belongs to a package.",
"type" : "string"
}
},
"homepage" : {
"type" : "string"
},
"licenseComments" : {
"description" : "The licenseComments property allows the preparer of the SPDX document to describe why the licensing in spdx:licenseConcluded was chosen.",
"type" : "string"
},
"licenseConcluded" : {
"description" : "License expression for licenseConcluded. See SPDX Annex D for the license expression syntax. The licensing that the preparer of this SPDX document has concluded, based on the evidence, actually applies to the SPDX Item.\n\nIf the licenseConcluded field is not present for an SPDX Item, it implies an equivalent meaning to NOASSERTION.",
"type" : "string"
},
"licenseDeclared" : {
"description" : "License expression for licenseDeclared. See SPDX Annex D for the license expression syntax. The licensing that the creators of the software in the package, or the packager, have declared. Declarations by the original software creator should be preferred, if they exist.",
"type" : "string"
},
"licenseInfoFromFiles" : {
"description" : "The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.\n\nIf the licenseInfoFromFiles field is not present for a package and filesAnalyzed property for that same pacakge is true or omitted, it implies an equivalent meaning to NOASSERTION.",
"type" : "array",
"items" : {
"description" : "License expression for licenseInfoFromFiles. See SPDX Annex D for the license expression syntax. The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.\n\nIf the licenseInfoFromFiles field is not present for a package and filesAnalyzed property for that same pacakge is true or omitted, it implies an equivalent meaning to NOASSERTION.",
"type" : "string"
}
},
"name" : {
"description" : "Identify name of this SpdxElement.",
"type" : "string"
},
"originator" : {
"description" : "The name and, optionally, contact information of the person or organization that originally created the package. Values of this property must conform to the agent and tool syntax.",
"type" : "string"
},
"packageFileName" : {
"description" : "The base name of the package file name. For example, zlib-1.2.5.tar.gz.",
"type" : "string"
},
"packageVerificationCode" : {
"type" : "object",
"properties" : {
"packageVerificationCodeExcludedFiles" : {
"description" : "A file that was excluded when calculating the package verification code. This is usually a file containing SPDX data regarding the package. If a package contains more than one SPDX file all SPDX files must be excluded from the package verification code. If this is not done it would be impossible to correctly calculate the verification codes in both files.",
"type" : "array",
"items" : {
"description" : "A file that was excluded when calculating the package verification code. This is usually a file containing SPDX data regarding the package. If a package contains more than one SPDX file all SPDX files must be excluded from the package verification code. If this is not done it would be impossible to correctly calculate the verification codes in both files.",
"type" : "string"
}
},
"packageVerificationCodeValue" : {
"description" : "The actual package verification code as a hex encoded value.",
"type" : "string"
}
},
"required" : [ "packageVerificationCodeValue" ],
"additionalProperties" : false,
"description" : "A manifest based verification code (the algorithm is defined in section 4.7 of the full specification) of the SPDX Item. This allows consumers of this data and/or database to determine if an SPDX item they have in hand is identical to the SPDX item from which the data was produced. This algorithm works even if the SPDX document is included in the SPDX item."
},
"primaryPackagePurpose" : {
"description" : "This field provides information about the primary purpose of the identified package. Package Purpose is intrinsic to how the package is being used rather than the content of the package.",
"type" : "string",
"enum" : [ "OTHER", "INSTALL", "ARCHIVE", "FIRMWARE", "APPLICATION", "FRAMEWORK", "LIBRARY", "CONTAINER", "SOURCE", "DEVICE", "OPERATING_SYSTEM", "FILE" ]
},
"releaseDate" : {
"description" : "This field provides a place for recording the date the package was released.",
"type" : "string"
},
"sourceInfo" : {
"description" : "Allows the producer(s) of the SPDX document to describe how the package was acquired and/or changed from the original source.",
"type" : "string"
},
"summary" : {
"description" : "Provides a short description of the package.",
"type" : "string"
},
"supplier" : {
"description" : "The name and, optionally, contact information of the person or organization who was the immediate supplier of this package to the recipient. The supplier may be different than originator when the software has been repackaged. Values of this property must conform to the agent and tool syntax.",
"type" : "string"
},
"validUntilDate" : {
"description" : "This field provides a place for recording the end of the support period for a package from the supplier.",
"type" : "string"
},
"versionInfo" : {
"description" : "Provides an indication of the version of the package that is described by this SpdxDocument.",
"type" : "string"
}
},
"required" : [ "SPDXID", "downloadLocation", "name" ],
"additionalProperties" : false
}
},
"files" : {
"description" : "Files referenced in the SPDX document",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"SPDXID" : {
"type" : "string",
"description" : "Uniquely identify any element in an SPDX document which may be referenced by other elements."
},
"annotations" : {
"description" : "Provide additional information about an SpdxElement.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"annotationDate" : {
"description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.",
"type" : "string"
},
"annotationType" : {
"description" : "Type of the annotation.",
"type" : "string",
"enum" : [ "OTHER", "REVIEW" ]
},
"annotator" : {
"description" : "This field identifies the person, organization, or tool that has commented on a file, package, snippet, or the entire document.",
"type" : "string"
},
"comment" : {
"type" : "string"
}
},
"required" : [ "annotationDate", "annotationType", "annotator", "comment" ],
"additionalProperties" : false,
"description" : "An Annotation is a comment on an SpdxItem by an agent."
}
},
"artifactOfs" : {
"description" : "Indicates the project in which the SpdxElement originated. Tools must preserve doap:homepage and doap:name properties and the URI (if one is known) of doap:Project resources that are values of this property. All other properties of doap:Projects are not directly supported by SPDX and may be dropped when translating to or from some SPDX formats.",
"type" : "array",
"items" : {
"type" : "object"
}
},
"attributionTexts" : {
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include the actual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
"type" : "array",
"items" : {
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include the actual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
"type" : "string"
}
},
"checksums" : {
"description" : "The checksum property provides a mechanism that can be used to verify that the contents of a File or Package have not changed.",
"minItems" : 1,
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"algorithm" : {
"description" : "Identifies the algorithm used to produce the subject Checksum. Currently, SHA-1 is the only supported algorithm. It is anticipated that other algorithms will be supported at a later time.",
"type" : "string",
"enum" : [ "SHA1", "BLAKE3", "SHA3-384", "SHA256", "SHA384", "BLAKE2b-512", "BLAKE2b-256", "SHA3-512", "MD2", "ADLER32", "MD4", "SHA3-256", "BLAKE2b-384", "SHA512", "MD6", "MD5", "SHA224" ]
},
"checksumValue" : {
"description" : "The checksumValue property provides a lower case hexidecimal encoded digest value produced using a specific algorithm.",
"type" : "string"
}
},
"required" : [ "algorithm", "checksumValue" ],
"additionalProperties" : false,
"description" : "A Checksum is value that allows the contents of a file to be authenticated. Even small changes to the content of the file will change its checksum. This class allows the results of a variety of checksum and cryptographic message digest algorithms to be represented."
}
},
"comment" : {
"type" : "string"
},
"copyrightText" : {
"description" : "The text of copyright declarations recited in the package, file or snippet.\n\nIf the copyrightText field is not present, it implies an equivalent meaning to NOASSERTION.",
"type" : "string"
},
"fileContributors" : {
"description" : "This field provides a place for the SPDX file creator to record file contributors. Contributors could include names of copyright holders and/or authors who may not be copyright holders yet contributed to the file content.",
"type" : "array",
"items" : {
"description" : "This field provides a place for the SPDX file creator to record file contributors. Contributors could include names of copyright holders and/or authors who may not be copyright holders yet contributed to the file content.",
"type" : "string"
}
},
"fileDependencies" : {
"description" : "This field is deprecated since SPDX 2.0 in favor of using Section 7 which provides more granularity about relationships.",
"type" : "array",
"items" : {
"description" : "SPDX ID for File. This field is deprecated since SPDX 2.0 in favor of using Section 7 which provides more granularity about relationships.",
"type" : "string"
}
},
"fileName" : {
"description" : "The name of the file relative to the root of the package.",
"type" : "string"
},
"fileTypes" : {
"description" : "The type of the file.",
"type" : "array",
"items" : {
"description" : "The type of the file.",
"type" : "string",
"enum" : [ "OTHER", "DOCUMENTATION", "IMAGE", "VIDEO", "ARCHIVE", "SPDX", "APPLICATION", "SOURCE", "BINARY", "TEXT", "AUDIO" ]
}
},
"licenseComments" : {
"description" : "The licenseComments property allows the preparer of the SPDX document to describe why the licensing in spdx:licenseConcluded was chosen.",
"type" : "string"
},
"licenseConcluded" : {
"description" : "License expression for licenseConcluded. See SPDX Annex D for the license expression syntax. The licensing that the preparer of this SPDX document has concluded, based on the evidence, actually applies to the SPDX Item.\n\nIf the licenseConcluded field is not present for an SPDX Item, it implies an equivalent meaning to NOASSERTION.",
"type" : "string"
},
"licenseInfoInFiles" : {
"description" : "Licensing information that was discovered directly in the subject file. This is also considered a declared license for the file.\n\nIf the licenseInfoInFile field is not present for a file, it implies an equivalent meaning to NOASSERTION.",
"type" : "array",
"items" : {
"description" : "License expression for licenseInfoInFile. See SPDX Annex D for the license expression syntax. Licensing information that was discovered directly in the subject file. This is also considered a declared license for the file.\n\nIf the licenseInfoInFile field is not present for a file, it implies an equivalent meaning to NOASSERTION.",
"type" : "string"
}
},
"noticeText" : {
"description" : "This field provides a place for the SPDX file creator to record potential legal notices found in the file. This may or may not include copyright statements.",
"type" : "string"
}
},
"required" : [ "SPDXID", "checksums", "fileName" ],
"additionalProperties" : false
}
},
"snippets" : {
"description" : "Snippets referenced in the SPDX document",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"SPDXID" : {
"type" : "string",
"description" : "Uniquely identify any element in an SPDX document which may be referenced by other elements."
},
"annotations" : {
"description" : "Provide additional information about an SpdxElement.",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"annotationDate" : {
"description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.",
"type" : "string"
},
"annotationType" : {
"description" : "Type of the annotation.",
"type" : "string",
"enum" : [ "OTHER", "REVIEW" ]
},
"annotator" : {
"description" : "This field identifies the person, organization, or tool that has commented on a file, package, snippet, or the entire document.",
"type" : "string"
},
"comment" : {
"type" : "string"
}
},
"required" : [ "annotationDate", "annotationType", "annotator", "comment" ],
"additionalProperties" : false,
"description" : "An Annotation is a comment on an SpdxItem by an agent."
}
},
"attributionTexts" : {
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include the actual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
"type" : "array",
"items" : {
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include the actual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
"type" : "string"
}
},
"comment" : {
"type" : "string"
},
"copyrightText" : {
"description" : "The text of copyright declarations recited in the package, file or snippet.\n\nIf the copyrightText field is not present, it implies an equivalent meaning to NOASSERTION.",
"type" : "string"
},
"licenseComments" : {
"description" : "The licenseComments property allows the preparer of the SPDX document to describe why the licensing in spdx:licenseConcluded was chosen.",
"type" : "string"
},
"licenseConcluded" : {
"description" : "License expression for licenseConcluded. See SPDX Annex D for the license expression syntax. The licensing that the preparer of this SPDX document has concluded, based on the evidence, actually applies to the SPDX Item.\n\nIf the licenseConcluded field is not present for an SPDX Item, it implies an equivalent meaning to NOASSERTION.",
"type" : "string"
},
"licenseInfoInSnippets" : {
"description" : "Licensing information that was discovered directly in the subject snippet. This is also considered a declared license for the snippet.\n\nIf the licenseInfoInSnippet field is not present for a snippet, it implies an equivalent meaning to NOASSERTION.",
"type" : "array",
"items" : {
"description" : "License expression for licenseInfoInSnippet. See SPDX Annex D for the license expression syntax. Licensing information that was discovered directly in the subject snippet. This is also considered a declared license for the snippet.\n\nIf the licenseInfoInSnippet field is not present for a snippet, it implies an equivalent meaning to NOASSERTION.",
"type" : "string"
}
},
"name" : {
"description" : "Identify name of this SpdxElement.",
"type" : "string"
},
"ranges" : {
"description" : "This field defines the byte range in the original host file (in X.2) that the snippet information applies to",
"minItems" : 1,
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"endPointer" : {
"type" : "object",
"properties" : {
"reference" : {
"description" : "SPDX ID for File",
"type" : "string"
},
"offset" : {
"type" : "integer",
"description" : "Byte offset in the file"
},
"lineNumber" : {
"type" : "integer",
"description" : "line number offset in the file"
}
},
"required" : [ "reference" ],
"additionalProperties" : false
},
"startPointer" : {
"type" : "object",
"properties" : {
"reference" : {
"description" : "SPDX ID for File",
"type" : "string"
},
"offset" : {
"type" : "integer",
"description" : "Byte offset in the file"
},
"lineNumber" : {
"type" : "integer",
"description" : "line number offset in the file"
}
},
"required" : [ "reference" ],
"additionalProperties" : false
}
},
"required" : [ "endPointer", "startPointer" ],
"additionalProperties" : false
}
},
"snippetFromFile" : {
"description" : "SPDX ID for File. File containing the SPDX element (e.g. the file contaning a snippet).",
"type" : "string"
}
},
"required" : [ "SPDXID", "name", "ranges", "snippetFromFile" ],
"additionalProperties" : false
}
},
"relationships" : {
"description" : "Relationships referenced in the SPDX document",
"type" : "array",
"items" : {
"type" : "object",
"properties" : {
"spdxElementId" : {
"type" : "string",
"description" : "Id to which the SPDX element is related"
},
"comment" : {
"type" : "string"
},
"relatedSpdxElement" : {
"description" : "SPDX ID for SpdxElement. A related SpdxElement.",
"type" : "string"
},
"relationshipType" : {
"description" : "Describes the type of relationship between two SPDX elements.",
"type" : "string",
"enum" : [ "VARIANT_OF", "COPY_OF", "PATCH_FOR", "TEST_DEPENDENCY_OF", "CONTAINED_BY", "DATA_FILE_OF", "OPTIONAL_COMPONENT_OF", "ANCESTOR_OF", "GENERATES", "CONTAINS", "OPTIONAL_DEPENDENCY_OF", "FILE_ADDED", "REQUIREMENT_DESCRIPTION_FOR", "DEV_DEPENDENCY_OF", "DEPENDENCY_OF", "BUILD_DEPENDENCY_OF", "DESCRIBES", "PREREQUISITE_FOR", "HAS_PREREQUISITE", "PROVIDED_DEPENDENCY_OF", "DYNAMIC_LINK", "DESCRIBED_BY", "METAFILE_OF", "DEPENDENCY_MANIFEST_OF", "PATCH_APPLIED", "RUNTIME_DEPENDENCY_OF", "TEST_OF", "TEST_TOOL_OF", "DEPENDS_ON", "SPECIFICATION_FOR", "FILE_MODIFIED", "DISTRIBUTION_ARTIFACT", "AMENDS", "DOCUMENTATION_OF", "GENERATED_FROM", "STATIC_LINK", "OTHER", "BUILD_TOOL_OF", "TEST_CASE_OF", "PACKAGE_OF", "DESCENDANT_OF", "FILE_DELETED", "EXPANDED_FROM_ARCHIVE", "DEV_TOOL_OF", "EXAMPLE_OF" ]
}
},
"required" : [ "spdxElementId", "relatedSpdxElement", "relationshipType" ],
"additionalProperties" : false
}
}
},
"required" : [ "SPDXID", "creationInfo", "dataLicense", "name", "spdxVersion", "documentNamespace" ],
"additionalProperties" : false
}
+9
View File
@@ -17,6 +17,15 @@ const (
DescribedByRelationship RelationshipType = "described-by"
)
func AllRelationshipTypes() []RelationshipType {
return []RelationshipType{
OwnershipByFileOverlapRelationship,
ContainsRelationship,
DependencyOfRelationship,
DescribedByRelationship,
}
}
type RelationshipType string
type Relationship struct {
@@ -1,4 +1,4 @@
package pkg
package cpe
import (
"sort"
@@ -6,15 +6,15 @@ import (
"github.com/facebookincubator/nvdtools/wfn"
)
var _ sort.Interface = (*CPEBySpecificity)(nil)
var _ sort.Interface = (*BySpecificity)(nil)
type CPEBySpecificity []wfn.Attributes
type BySpecificity []wfn.Attributes
func (c CPEBySpecificity) Len() int { return len(c) }
func (c BySpecificity) Len() int { return len(c) }
func (c CPEBySpecificity) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c BySpecificity) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c CPEBySpecificity) Less(i, j int) bool {
func (c BySpecificity) Less(i, j int) bool {
iScore := weightedCountForSpecifiedFields(c[i])
jScore := weightedCountForSpecifiedFields(c[j])
@@ -29,7 +29,7 @@ func (c CPEBySpecificity) Less(i, j int) bool {
}
// if score and length are equal then text sort
// note that we are not using CPEString from the syft pkg
// note that we are not using String from the syft pkg
// as we are not encoding/decoding this CPE string so we don't
// need the proper quoted version of the CPE.
return c[i].BindToFmtString() < c[j].BindToFmtString()
+98
View File
@@ -0,0 +1,98 @@
package cpe
import (
"sort"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_BySpecificity(t *testing.T) {
tests := []struct {
name string
input []CPE
expected []CPE
}{
{
name: "sort strictly by wfn *",
input: []CPE{
Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"),
Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"),
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"),
},
expected: []CPE{
Must("cpe:2.3:a:some:package:1:*:*:*:*:some:*:*"),
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:*:package:1:*:*:*:*:some:*:*"),
Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"),
},
},
{
name: "sort strictly by field length",
input: []CPE{
Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"),
},
expected: []CPE{
Must("cpe:2.3:a:1:666666:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:1:1:*:*:*:*:4444:*:*"),
Must("cpe:2.3:a:1:1:333:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
},
},
{
name: "sort by mix of field length and specificity",
input: []CPE{
Must("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"),
Must("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
},
expected: []CPE{
Must("cpe:2.3:a:55555:1:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:22:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:1:1:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:1:666666:*:*:*:*:*:1:*:*"),
Must("cpe:2.3:a:*:1:1:*:*:*:*:4444:*:*"),
Must("cpe:2.3:a:1:*:333:*:*:*:*:*:*:*"),
},
},
{
name: "sort by mix of field length, specificity, dash",
input: []CPE{
Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
},
expected: []CPE{
Must("cpe:2.3:a:alpine-keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine-keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine_keys:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine_keys:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
sort.Sort(BySpecificity(test.input))
assert.Equal(t, test.expected, test.input)
})
}
}
+29 -28
View File
@@ -1,4 +1,4 @@
package pkg
package cpe
import (
"fmt"
@@ -24,17 +24,17 @@ const cpeRegexString = ((`^([c][pP][eE]:/[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6})`)
var cpeRegex = regexp.MustCompile(cpeRegexString)
// NewCPE will parse a formatted CPE string and return a CPE object. Some input, such as the existence of whitespace
// New will parse a formatted CPE string and return a CPE object. Some input, such as the existence of whitespace
// characters is allowed, however, a more strict validation is done after this sanitization process.
func NewCPE(cpeStr string) (CPE, error) {
func New(cpeStr string) (CPE, error) {
// get a CPE object based on the given string --don't validate yet since it may be possible to escape select cases on the callers behalf
c, err := newCPEWithoutValidation(cpeStr)
c, err := newWithoutValidation(cpeStr)
if err != nil {
return CPE{}, fmt.Errorf("unable to parse CPE string: %w", err)
}
// ensure that this CPE can be validated after being fully sanitized
if ValidateCPEString(CPEString(c)) != nil {
if ValidateString(String(c)) != nil {
return CPE{}, err
}
@@ -43,7 +43,16 @@ func NewCPE(cpeStr string) (CPE, error) {
return c, nil
}
func ValidateCPEString(cpeStr string) error {
// Must returns a CPE or panics if the provided string is not valid
func Must(cpeStr string) CPE {
c, err := New(cpeStr)
if err != nil {
panic(err)
}
return c
}
func ValidateString(cpeStr string) error {
// We should filter out all CPEs that do not match the official CPE regex
// The facebook nvdtools parser can sometimes incorrectly parse invalid CPE strings
if !cpeRegex.MatchString(cpeStr) {
@@ -52,7 +61,7 @@ func ValidateCPEString(cpeStr string) error {
return nil
}
func newCPEWithoutValidation(cpeStr string) (CPE, error) {
func newWithoutValidation(cpeStr string) (CPE, error) {
value, err := wfn.Parse(cpeStr)
if err != nil {
return CPE{}, fmt.Errorf("failed to parse CPE=%q: %w", cpeStr, err)
@@ -63,30 +72,22 @@ func newCPEWithoutValidation(cpeStr string) (CPE, error) {
}
// we need to compare the raw data since we are constructing CPEs in other locations
value.Vendor = normalizeCpeField(value.Vendor)
value.Product = normalizeCpeField(value.Product)
value.Language = normalizeCpeField(value.Language)
value.Version = normalizeCpeField(value.Version)
value.TargetSW = normalizeCpeField(value.TargetSW)
value.Part = normalizeCpeField(value.Part)
value.Edition = normalizeCpeField(value.Edition)
value.Other = normalizeCpeField(value.Other)
value.SWEdition = normalizeCpeField(value.SWEdition)
value.TargetHW = normalizeCpeField(value.TargetHW)
value.Update = normalizeCpeField(value.Update)
value.Vendor = normalizeField(value.Vendor)
value.Product = normalizeField(value.Product)
value.Language = normalizeField(value.Language)
value.Version = normalizeField(value.Version)
value.TargetSW = normalizeField(value.TargetSW)
value.Part = normalizeField(value.Part)
value.Edition = normalizeField(value.Edition)
value.Other = normalizeField(value.Other)
value.SWEdition = normalizeField(value.SWEdition)
value.TargetHW = normalizeField(value.TargetHW)
value.Update = normalizeField(value.Update)
return *value, nil
}
func MustCPE(cpeStr string) CPE {
c, err := NewCPE(cpeStr)
if err != nil {
panic(err)
}
return c
}
func normalizeCpeField(field string) string {
func normalizeField(field string) string {
// replace spaces with underscores (per section 5.3.2 of the CPE spec v 2.3)
field = strings.ReplaceAll(field, " ", "_")
@@ -112,7 +113,7 @@ func stripSlashes(s string) string {
return sb.String()
}
func CPEString(c CPE) string {
func String(c CPE) string {
output := CPE{}
output.Vendor = sanitize(c.Vendor)
output.Product = sanitize(c.Product)
+19 -26
View File
@@ -1,4 +1,4 @@
package pkg
package cpe
import (
"encoding/json"
@@ -11,14 +11,7 @@ import (
"github.com/stretchr/testify/require"
)
func must(c CPE, e error) CPE {
if e != nil {
panic(e)
}
return c
}
func TestNewCPE(t *testing.T) {
func Test_New(t *testing.T) {
tests := []struct {
name string
input string
@@ -27,29 +20,29 @@ func TestNewCPE(t *testing.T) {
{
name: "gocase",
input: `cpe:/a:10web:form_maker:1.0.0::~~~wordpress~~`,
expected: must(NewCPE(`cpe:2.3:a:10web:form_maker:1.0.0:*:*:*:*:wordpress:*:*`)),
expected: Must(`cpe:2.3:a:10web:form_maker:1.0.0:*:*:*:*:wordpress:*:*`),
},
{
name: "dashes",
input: `cpe:/a:7-zip:7-zip:4.56:beta:~~~windows~~`,
expected: must(NewCPE(`cpe:2.3:a:7-zip:7-zip:4.56:beta:*:*:*:windows:*:*`)),
expected: Must(`cpe:2.3:a:7-zip:7-zip:4.56:beta:*:*:*:windows:*:*`),
},
{
name: "URL escape characters",
input: `cpe:/a:%240.99_kindle_books_project:%240.99_kindle_books:6::~~~android~~`,
expected: must(NewCPE(`cpe:2.3:a:\$0.99_kindle_books_project:\$0.99_kindle_books:6:*:*:*:*:android:*:*`)),
expected: Must(`cpe:2.3:a:\$0.99_kindle_books_project:\$0.99_kindle_books:6:*:*:*:*:android:*:*`),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual, err := NewCPE(test.input)
actual, err := New(test.input)
if err != nil {
t.Fatalf("got an error while creating CPE: %+v", err)
}
if CPEString(actual) != CPEString(test.expected) {
t.Errorf("mismatched entries:\n\texpected:%+v\n\t actual:%+v\n", CPEString(test.expected), CPEString(actual))
if String(actual) != String(test.expected) {
t.Errorf("mismatched entries:\n\texpected:%+v\n\t actual:%+v\n", String(test.expected), String(actual))
}
})
@@ -81,7 +74,7 @@ func Test_normalizeCpeField(t *testing.T) {
}
for _, test := range tests {
t.Run(test.field, func(t *testing.T) {
assert.Equal(t, test.expected, normalizeCpeField(test.field))
assert.Equal(t, test.expected, normalizeField(test.field))
})
}
}
@@ -98,14 +91,14 @@ func Test_CPEParser(t *testing.T) {
for _, test := range testCases {
t.Run(test.CPEString, func(t *testing.T) {
c1, err := NewCPE(test.CPEString)
c1, err := New(test.CPEString)
assert.NoError(t, err)
c2, err := NewCPE(test.CPEUrl)
c2, err := New(test.CPEUrl)
assert.NoError(t, err)
assert.Equal(t, c1, c2)
assert.Equal(t, c1, test.WFN)
assert.Equal(t, c2, test.WFN)
assert.Equal(t, CPEString(test.WFN), test.CPEString)
assert.Equal(t, String(test.WFN), test.CPEString)
})
}
}
@@ -167,16 +160,16 @@ func Test_InvalidCPE(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c, err := NewCPE(test.in)
c, err := New(test.in)
if test.expectedErr {
assert.Error(t, err)
if t.Failed() {
t.Logf("got CPE: %q details: %+v", CPEString(c), c)
t.Logf("got CPE: %q details: %+v", String(c), c)
}
return
}
require.NoError(t, err)
assert.Equal(t, test.expected, CPEString(c))
assert.Equal(t, test.expected, String(c))
})
}
}
@@ -222,13 +215,13 @@ func Test_RoundTrip(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// CPE string must be preserved through a round trip
assert.Equal(t, test.cpe, CPEString(MustCPE(test.cpe)))
assert.Equal(t, test.cpe, String(Must(test.cpe)))
// The parsed CPE must be the same after a round trip
assert.Equal(t, MustCPE(test.cpe), MustCPE(CPEString(MustCPE(test.cpe))))
assert.Equal(t, Must(test.cpe), Must(String(Must(test.cpe))))
// The test case parsed CPE must be the same after parsing the input string
assert.Equal(t, test.parsedCPE, MustCPE(test.cpe))
assert.Equal(t, test.parsedCPE, Must(test.cpe))
// The test case parsed CPE must produce the same string as the input cpe
assert.Equal(t, CPEString(test.parsedCPE), test.cpe)
assert.Equal(t, String(test.parsedCPE), test.cpe)
})
}
}
@@ -1,10 +1,10 @@
package pkg
package cpe
import (
"sort"
)
func mergeCPEs(a, b []CPE) (result []CPE) {
func Merge(a, b []CPE) (result []CPE) {
aCPEs := make(map[string]CPE)
// keep all CPEs from a and create a quick string-based lookup
@@ -20,6 +20,6 @@ func mergeCPEs(a, b []CPE) (result []CPE) {
}
}
sort.Sort(CPEBySpecificity(result))
sort.Sort(BySpecificity(result))
return result
}
+41
View File
@@ -0,0 +1,41 @@
package cpe
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_Merge(t *testing.T) {
tests := []struct {
name string
input [][]CPE
expected []CPE
}{
{
name: "merge, removing duplicates and ordered",
input: [][]CPE{
{
Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"),
},
{
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"),
},
},
expected: []CPE{
Must("cpe:2.3:a:some:package:1:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:some:package:*:*:*:*:*:*:*:*"),
Must("cpe:2.3:a:*:package:1:*:*:*:*:*:*:*"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
out := Merge(test.input[0], test.input[1])
assert.Equal(t, test.expected, out)
})
}
}
+2 -2
View File
@@ -32,6 +32,6 @@ const (
// ImportStarted is a partybus event that occurs when an SBOM upload process has begun
ImportStarted partybus.EventType = "syft-import-started-event"
// UploadAttestation is a partybus event that occurs when syft uploads an attestation to an OCI registry (+ any transparency log)
UploadAttestation partybus.EventType = "syft-upload-attestation"
// AttestationStarted is a partybus event that occurs when starting an SBOM attestation process
AttestationStarted partybus.EventType = "syft-attestation-started-event"
)
+23
View File
@@ -0,0 +1,23 @@
package monitor
import (
"io"
"github.com/wagoodman/go-progress"
)
type ShellProgress struct {
io.Reader
*progress.Manual
}
type Title struct {
Default string
WhileRunning string
OnSuccess string
}
type GenericTask struct {
Title Title
Context string
}
+13 -6
View File
@@ -5,11 +5,13 @@ package parsers
import (
"fmt"
"io"
"github.com/wagoodman/go-partybus"
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/event/monitor"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg/cataloger"
)
@@ -153,15 +155,20 @@ func ParseImportStarted(e partybus.Event) (string, progress.StagedProgressable,
return host, prog, nil
}
func ParseUploadAttestation(e partybus.Event) (progress.StagedProgressable, error) {
if err := checkEventType(e.Type, event.UploadAttestation); err != nil {
return nil, err
func ParseAttestationStartedEvent(e partybus.Event) (io.Reader, progress.Progressable, *monitor.GenericTask, error) {
if err := checkEventType(e.Type, event.AttestationStarted); err != nil {
return nil, nil, nil, err
}
prog, ok := e.Value.(progress.StagedProgressable)
source, ok := e.Source.(monitor.GenericTask)
if !ok {
return nil, newPayloadErr(e.Type, "Value", e.Value)
return nil, nil, nil, newPayloadErr(e.Type, "Source", e.Source)
}
return prog, nil
sp, ok := e.Value.(*monitor.ShellProgress)
if !ok {
return nil, nil, nil, newPayloadErr(e.Type, "Value", e.Value)
}
return sp.Reader, sp.Manual, &source, nil
}
-38
View File
@@ -1,38 +0,0 @@
package file
import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/source"
)
type ClassificationCataloger struct {
classifiers []Classifier
}
func NewClassificationCataloger(classifiers []Classifier) (*ClassificationCataloger, error) {
return &ClassificationCataloger{
classifiers: classifiers,
}, nil
}
func (i *ClassificationCataloger) Catalog(resolver source.FileResolver) (map[source.Coordinates][]Classification, error) {
results := make(map[source.Coordinates][]Classification)
numResults := 0
for _, location := range allRegularFiles(resolver) {
for _, classifier := range i.classifiers {
result, err := classifier.Classify(resolver, location)
if err != nil {
log.Warnf("file classification cataloger failed with class=%q at location=%+v: %+v", classifier.Class, location, err)
continue
}
if result != nil {
results[location.Coordinates] = append(results[location.Coordinates], *result)
numResults++
}
}
}
log.Debugf("file classifier discovered %d results", numResults)
return results, nil
}
-210
View File
@@ -1,210 +0,0 @@
package file
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/source"
)
func TestClassifierCataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
tests := []struct {
name string
fixtureDir string
location string
expected []Classification
expectedErr func(assert.TestingT, error, ...interface{}) bool
}{
{
name: "positive-libpython3.7.so",
fixtureDir: "test-fixtures/classifiers/positive",
location: "libpython3.7.so",
expected: []Classification{
{
Class: "python-binary",
Metadata: map[string]string{
"version": "3.7.4a-vZ9",
},
},
},
expectedErr: assert.NoError,
},
{
name: "positive-python3.6",
fixtureDir: "test-fixtures/classifiers/positive",
location: "python3.6",
expected: []Classification{
{
Class: "python-binary",
Metadata: map[string]string{
"version": "3.6.3a-vZ9",
},
},
},
expectedErr: assert.NoError,
},
{
name: "positive-patchlevel.h",
fixtureDir: "test-fixtures/classifiers/positive",
location: "patchlevel.h",
expected: []Classification{
{
Class: "cpython-source",
Metadata: map[string]string{
"version": "3.9-aZ5",
},
},
},
expectedErr: assert.NoError,
},
{
name: "positive-go",
fixtureDir: "test-fixtures/classifiers/positive",
location: "go",
expected: []Classification{
{
Class: "go-binary",
Metadata: map[string]string{
"version": "1.14",
},
},
},
expectedErr: assert.NoError,
},
{
name: "positive-go-hint",
fixtureDir: "test-fixtures/classifiers/positive",
location: "VERSION",
expected: []Classification{
{
Class: "go-binary-hint",
Metadata: map[string]string{
"version": "1.15",
},
},
},
expectedErr: assert.NoError,
},
{
name: "positive-busybox",
fixtureDir: "test-fixtures/classifiers/positive",
location: "[", // note: busybox is a link to [
expected: []Classification{
{
Class: "busybox-binary",
VirtualPath: "busybox",
Metadata: map[string]string{
"version": "3.33.3",
},
},
},
expectedErr: assert.NoError,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c, err := NewClassificationCataloger(DefaultClassifiers)
test.expectedErr(t, err)
src, err := source.NewFromDirectory(test.fixtureDir)
test.expectedErr(t, err)
resolver, err := src.FileResolver(source.SquashedScope)
test.expectedErr(t, err)
actualResults, err := c.Catalog(resolver)
test.expectedErr(t, err)
ok := false
for actualLoc, actualClassification := range actualResults {
if test.location == actualLoc.RealPath {
ok = true
assert.Equal(t, test.expected, actualClassification)
}
}
if !ok {
t.Fatalf("could not find test location=%q", test.location)
}
})
}
}
func TestClassifierCataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
tests := []struct {
name string
fixtureImage string
location string
expected []Classification
expectedErr func(assert.TestingT, error, ...interface{}) bool
}{
{
name: "busybox-regression",
fixtureImage: "image-busybox",
location: "/bin/[",
expected: []Classification{
{
Class: "busybox-binary",
VirtualPath: "/bin/busybox",
Metadata: map[string]string{
"version": "1.35.0",
},
},
},
expectedErr: assert.NoError,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c, err := NewClassificationCataloger(DefaultClassifiers)
test.expectedErr(t, err)
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage)
src, err := source.NewFromImage(img, "test-img")
test.expectedErr(t, err)
resolver, err := src.FileResolver(source.SquashedScope)
test.expectedErr(t, err)
actualResults, err := c.Catalog(resolver)
test.expectedErr(t, err)
ok := false
for actuaLoc, actualClassification := range actualResults {
if actuaLoc.RealPath == test.location {
ok = true
assert.Equal(t, test.expected, actualClassification)
}
}
if !ok {
t.Fatalf("could not find test location=%q", test.location)
}
})
}
}
func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) {
c, err := NewClassificationCataloger(DefaultClassifiers)
assert.NoError(t, err)
src, err := source.NewFromDirectory("test-fixtures/classifiers/negative")
assert.NoError(t, err)
resolver, err := src.FileResolver(source.SquashedScope)
assert.NoError(t, err)
actualResults, err := c.Catalog(resolver)
assert.NoError(t, err)
assert.Equal(t, 0, len(actualResults))
}
-153
View File
@@ -1,153 +0,0 @@
package file
import (
"bytes"
"fmt"
"io"
"regexp"
"text/template"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/source"
)
var DefaultClassifiers = []Classifier{
{
Class: "python-binary",
FilepathPatterns: []*regexp.Regexp{
regexp.MustCompile(`(.*/|^)python(?P<version>[0-9]+\.[0-9]+)$`),
regexp.MustCompile(`(.*/|^)libpython(?P<version>[0-9]+\.[0-9]+).so.*$`),
},
EvidencePatternTemplates: []string{
`(?m)(?P<version>{{ .version }}\.[0-9]+[-_a-zA-Z0-9]*)`,
},
},
{
Class: "cpython-source",
FilepathPatterns: []*regexp.Regexp{
regexp.MustCompile(`(.*/|^)patchlevel.h$`),
},
EvidencePatternTemplates: []string{
`(?m)#define\s+PY_VERSION\s+"?(?P<version>[0-9\.\-_a-zA-Z]+)"?`,
},
},
{
Class: "go-binary",
FilepathPatterns: []*regexp.Regexp{
regexp.MustCompile(`(.*/|^)go$`),
},
EvidencePatternTemplates: []string{
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+)?)`,
},
},
{
Class: "nodejs-binary",
FilepathPatterns: []*regexp.Regexp{
regexp.MustCompile(`(.*/|^)node$`),
},
EvidencePatternTemplates: []string{
// regex that matches node.js/vx.y.z
`(?m)node\.js\/v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`,
},
},
{
Class: "go-binary-hint",
FilepathPatterns: []*regexp.Regexp{
regexp.MustCompile(`(.*/|^)VERSION$`),
},
EvidencePatternTemplates: []string{
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+)?)`,
},
},
{
Class: "busybox-binary",
FilepathPatterns: []*regexp.Regexp{
regexp.MustCompile(`(.*/|^)busybox$`),
},
EvidencePatternTemplates: []string{
`(?m)BusyBox\s+v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`,
},
},
}
type Classifier struct {
Class string
FilepathPatterns []*regexp.Regexp
EvidencePatternTemplates []string
}
type Classification struct {
Class string `json:"class"`
VirtualPath string `json:"virtual_path"`
Metadata map[string]string `json:"metadata"`
}
func (c Classifier) Classify(resolver source.FileResolver, location source.Location) (*Classification, error) {
doesFilepathMatch, filepathNamedGroupValues := FilepathMatches(c.FilepathPatterns, location)
if !doesFilepathMatch {
return nil, nil
}
contentReader, err := resolver.FileContentsByLocation(location)
if err != nil {
return nil, err
}
defer internal.CloseAndLogError(contentReader, location.VirtualPath)
// TODO: there is room for improvement here, as this may use an excessive amount of memory. Alternate approach is to leverage a RuneReader.
contents, err := io.ReadAll(contentReader)
if err != nil {
return nil, err
}
var result *Classification
for _, patternTemplate := range c.EvidencePatternTemplates {
tmpl, err := template.New("").Parse(patternTemplate)
if err != nil {
return nil, fmt.Errorf("unable to parse classifier template=%q : %w", patternTemplate, err)
}
patternBuf := &bytes.Buffer{}
err = tmpl.Execute(patternBuf, filepathNamedGroupValues)
if err != nil {
return nil, fmt.Errorf("unable to render template: %w", err)
}
pattern, err := regexp.Compile(patternBuf.String())
if err != nil {
return nil, fmt.Errorf("unable to compile rendered regex=%q: %w", patternBuf.String(), err)
}
if !pattern.Match(contents) {
continue
}
matchMetadata := internal.MatchNamedCaptureGroups(pattern, string(contents))
if result == nil {
result = &Classification{
Class: c.Class,
VirtualPath: location.VirtualPath,
Metadata: matchMetadata,
}
} else {
for key, value := range matchMetadata {
result.Metadata[key] = value
}
}
}
return result, nil
}
func FilepathMatches(patterns []*regexp.Regexp, location source.Location) (bool, map[string]string) {
for _, path := range []string{location.RealPath, location.VirtualPath} {
if path == "" {
continue
}
for _, pattern := range patterns {
if pattern.MatchString(path) {
return true, internal.MatchNamedCaptureGroups(pattern, path)
}
}
}
return false, nil
}
-97
View File
@@ -1,97 +0,0 @@
package file
import (
"regexp"
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/source"
)
func TestFilepathMatches(t *testing.T) {
tests := []struct {
name string
location source.Location
patterns []string
expectedMatches bool
expectedNamedGroups map[string]string
}{
{
name: "simple-filename-match",
location: source.Location{
Coordinates: source.Coordinates{
RealPath: "python2.7",
},
},
patterns: []string{
`python([0-9]+\.[0-9]+)$`,
},
expectedMatches: true,
},
{
name: "filepath-match",
location: source.Location{
Coordinates: source.Coordinates{
RealPath: "/usr/bin/python2.7",
},
},
patterns: []string{
`python([0-9]+\.[0-9]+)$`,
},
expectedMatches: true,
},
{
name: "virtual-filepath-match",
location: source.Location{
VirtualPath: "/usr/bin/python2.7",
},
patterns: []string{
`python([0-9]+\.[0-9]+)$`,
},
expectedMatches: true,
},
{
name: "full-filepath-match",
location: source.Location{
VirtualPath: "/usr/bin/python2.7",
},
patterns: []string{
`.*/bin/python([0-9]+\.[0-9]+)$`,
},
expectedMatches: true,
},
{
name: "anchored-filename-match-FAILS",
location: source.Location{
Coordinates: source.Coordinates{
RealPath: "/usr/bin/python2.7",
},
},
patterns: []string{
`^python([0-9]+\.[0-9]+)$`,
},
expectedMatches: false,
},
{
name: "empty-filename-match-FAILS",
location: source.Location{},
patterns: []string{
`^python([0-9]+\.[0-9]+)$`,
},
expectedMatches: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var patterns []*regexp.Regexp
for _, p := range test.patterns {
patterns = append(patterns, regexp.MustCompile(p))
}
actualMatches, actualNamedGroups := FilepathMatches(patterns, test.location)
assert.Equal(t, test.expectedMatches, actualMatches)
assert.Equal(t, test.expectedNamedGroups, actualNamedGroups)
})
}
}
+1 -5
View File
@@ -92,15 +92,11 @@ func DigestsFromFile(closer io.ReadCloser, hashes []crypto.Hash) ([]Digest, erro
writers[idx] = hashers[idx]
}
size, err := io.Copy(io.MultiWriter(writers...), closer)
_, err := io.Copy(io.MultiWriter(writers...), closer)
if err != nil {
return nil, err
}
if size == 0 {
return make([]Digest, 0), nil
}
result := make([]Digest, len(hashes))
// only capture digests when there is content. It is important to do this based on SIZE and not
// FILE TYPE. The reasoning is that it is possible for a tar to be crafted with a header-only
-6
View File
@@ -29,12 +29,6 @@ func testDigests(t testing.TB, root string, files []string, hashes ...crypto.Has
t.Fatalf("could not read %q : %+v", f, err)
}
if len(b) == 0 {
// we don't keep digests for empty files
digests[source.NewLocation(f).Coordinates] = []Digest{}
continue
}
for _, hash := range hashes {
h := hash.New()
h.Write(b)
@@ -1 +0,0 @@
!libpython3.7.so
@@ -1 +0,0 @@
go1.14
@@ -27,8 +27,13 @@ func encodeComponent(p pkg.Package) cyclonedx.Component {
properties = &props
}
componentType := cyclonedx.ComponentTypeLibrary
if p.Type == pkg.BinaryPkg {
componentType = cyclonedx.ComponentTypeApplication
}
return cyclonedx.Component{
Type: cyclonedx.ComponentTypeLibrary,
Type: componentType,
Name: p.Name,
Group: encodeGroup(p),
Version: p.Version,
@@ -90,8 +90,8 @@ func Test_encodeComponentProperties(t *testing.T) {
Version: "v0.0.0-20211006190231-62292e806868",
Language: pkg.Go,
Type: pkg.GoModulePkg,
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
MetadataType: pkg.GolangMetadataType,
Metadata: pkg.GolangMetadata{
GoCompiledVersion: "1.17",
Architecture: "amd64",
H1Digest: "h1:KlOXYy8wQWTUJYFgkUI40Lzr06ofg5IRXUK5C7qZt1k=",
@@ -99,7 +99,7 @@ func Test_encodeComponentProperties(t *testing.T) {
},
expected: &[]cyclonedx.Property{
{Name: "syft:package:language", Value: pkg.Go.String()},
{Name: "syft:package:metadataType", Value: "GolangBinMetadata"},
{Name: "syft:package:metadataType", Value: "GolangMetadata"},
{Name: "syft:package:type", Value: "go-module"},
{Name: "syft:metadata:architecture", Value: "amd64"},
{Name: "syft:metadata:goCompiledVersion", Value: "1.17"},
@@ -144,6 +144,60 @@ func Test_encodeComponentProperties(t *testing.T) {
}
}
func Test_encodeCompomentType(t *testing.T) {
tests := []struct {
name string
pkg pkg.Package
want cyclonedx.Component
}{
{
name: "non-binary package",
pkg: pkg.Package{
Name: "pkg1",
Version: "1.9.2",
Type: pkg.GoModulePkg,
},
want: cyclonedx.Component{
Name: "pkg1",
Version: "1.9.2",
Type: cyclonedx.ComponentTypeLibrary,
Properties: &[]cyclonedx.Property{
{
Name: "syft:package:type",
Value: "go-module",
},
},
},
},
{
name: "non-binary package",
pkg: pkg.Package{
Name: "pkg1",
Version: "3.1.2",
Type: pkg.BinaryPkg,
},
want: cyclonedx.Component{
Name: "pkg1",
Version: "3.1.2",
Type: cyclonedx.ComponentTypeApplication,
Properties: &[]cyclonedx.Property{
{
Name: "syft:package:type",
Value: "binary",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.pkg.ID()
p := encodeComponent(tt.pkg)
assert.Equal(t, tt.want, p)
})
}
}
func Test_deriveBomRef(t *testing.T) {
pkgWithPurl := pkg.Package{
Name: "django",
+6 -5
View File
@@ -4,6 +4,7 @@ import (
"github.com/CycloneDX/cyclonedx-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg"
)
@@ -11,7 +12,7 @@ func encodeSingleCPE(p pkg.Package) string {
// Since the CPEs in a package are sorted by specificity
// we can extract the first CPE as the one to output in cyclonedx
if len(p.CPEs) > 0 {
return pkg.CPEString(p.CPEs[0])
return cpe.String(p.CPEs[0])
}
return ""
}
@@ -24,15 +25,15 @@ func encodeCPEs(p pkg.Package) (out []cyclonedx.Property) {
}
out = append(out, cyclonedx.Property{
Name: "syft:cpe23",
Value: pkg.CPEString(c),
Value: cpe.String(c),
})
}
return
}
func decodeCPEs(c *cyclonedx.Component) (out []pkg.CPE) {
func decodeCPEs(c *cyclonedx.Component) (out []cpe.CPE) {
if c.CPE != "" {
cp, err := pkg.NewCPE(c.CPE)
cp, err := cpe.New(c.CPE)
if err != nil {
log.Warnf("invalid CPE: %s", c.CPE)
} else {
@@ -43,7 +44,7 @@ func decodeCPEs(c *cyclonedx.Component) (out []pkg.CPE) {
if c.Properties != nil {
for _, p := range *c.Properties {
if p.Name == "syft:cpe23" {
cp, err := pkg.NewCPE(p.Value)
cp, err := cpe.New(p.Value)
if err != nil {
log.Warnf("invalid CPE: %s", p.Value)
} else {
@@ -5,12 +5,13 @@ import (
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg"
)
func Test_encodeCPE(t *testing.T) {
testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
testCPE2 := pkg.MustCPE("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*")
testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
testCPE2 := cpe.Must("cpe:2.3:a:name:name2:3.2:*:*:*:*:*:*:*")
tests := []struct {
name string
input pkg.Package
@@ -20,14 +21,14 @@ func Test_encodeCPE(t *testing.T) {
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "no metadata",
input: pkg.Package{
CPEs: []pkg.CPE{},
CPEs: []cpe.CPE{},
},
expected: "",
},
{
name: "single CPE",
input: pkg.Package{
CPEs: []pkg.CPE{
CPEs: []cpe.CPE{
testCPE,
},
},
@@ -36,7 +37,7 @@ func Test_encodeCPE(t *testing.T) {
{
name: "multiple CPEs",
input: pkg.Package{
CPEs: []pkg.CPE{
CPEs: []cpe.CPE{
testCPE2,
testCPE,
},
@@ -216,7 +216,7 @@ func collectRelationships(bom *cyclonedx.BOM, s *sbom.SBOM, idMap map[string]int
}
for _, t := range *d.Dependencies {
to, toExists := idMap[t.Ref].(artifact.Identifiable)
to, toExists := idMap[t].(artifact.Identifiable)
if !toExists {
continue
}
@@ -92,7 +92,7 @@ func Test_decode(t *testing.T) {
},
},
CPE: "cpe:2.3:*:another:package:2:*:*:*:*:*:*:*",
PackageURL: "pkg:alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2",
PackageURL: "pkg:apk/alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2",
Properties: &[]cyclonedx.Property{
{
@@ -173,12 +173,8 @@ func Test_decode(t *testing.T) {
},
Dependencies: &[]cyclonedx.Dependency{
{
Ref: "p1",
Dependencies: &[]cyclonedx.Dependency{
{
Ref: "p2",
},
},
Ref: "p1",
Dependencies: &[]string{"p2"},
},
},
},
@@ -197,7 +193,7 @@ func Test_decode(t *testing.T) {
{
pkg: "package-2",
ver: "2.0.2",
purl: "pkg:alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2",
purl: "pkg:apk/alpine/alpine-baselayout@3.2.0-r16?arch=x86_64&upstream=alpine-baselayout&distro=alpine-3.14.2",
},
},
},
@@ -263,8 +259,9 @@ func Test_decode(t *testing.T) {
func Test_missingDataDecode(t *testing.T) {
bom := &cyclonedx.BOM{
Metadata: nil,
Components: &[]cyclonedx.Component{},
Metadata: nil,
Components: &[]cyclonedx.Component{},
SpecVersion: cyclonedx.SpecVersion1_4,
}
_, err := ToSyftModel(bom)
@@ -287,7 +284,9 @@ func Test_missingDataDecode(t *testing.T) {
}
func Test_missingComponentsDecode(t *testing.T) {
bom := &cyclonedx.BOM{}
bom := &cyclonedx.BOM{
SpecVersion: cyclonedx.SpecVersion1_4,
}
bomBytes, _ := json.Marshal(&bom)
decode := GetDecoder(cyclonedx.BOMFileFormatJSON)
@@ -9,6 +9,7 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
@@ -100,12 +101,12 @@ func toOSComponent(distro *linux.Release) []cyclonedx.Component {
}
func formatCPE(cpeString string) string {
cpe, err := pkg.NewCPE(cpeString)
c, err := cpe.New(cpeString)
if err != nil {
log.Debugf("skipping invalid CPE: %s", cpeString)
return ""
}
return pkg.CPEString(cpe)
return cpe.String(c)
}
// NewBomDescriptor returns a new BomDescriptor tailored for the current time and "syft" tool details.
@@ -157,8 +158,10 @@ func toDependencies(relationships []artifact.Relationship) []cyclonedx.Dependenc
continue
}
innerDeps := []cyclonedx.Dependency{}
innerDeps = append(innerDeps, cyclonedx.Dependency{Ref: deriveBomRef(*fromPkg)})
// ind dep
innerDeps := []string{}
innerDeps = append(innerDeps, deriveBomRef(*fromPkg))
result = append(result, cyclonedx.Dependency{
Ref: deriveBomRef(*toPkg),
Dependencies: &innerDeps,
@@ -1,6 +1,7 @@
package spdxhelpers
import (
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg"
)
@@ -10,7 +11,7 @@ func ExternalRefs(p pkg.Package) (externalRefs []ExternalRef) {
for _, c := range p.CPEs {
externalRefs = append(externalRefs, ExternalRef{
ReferenceCategory: SecurityReferenceCategory,
ReferenceLocator: pkg.CPEString(c),
ReferenceLocator: cpe.String(c),
ReferenceType: Cpe23ExternalRefType,
})
}
@@ -5,11 +5,12 @@ import (
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg"
)
func Test_ExternalRefs(t *testing.T) {
testCPE := pkg.MustCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
testCPE := cpe.Must("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*")
tests := []struct {
name string
input pkg.Package
@@ -18,7 +19,7 @@ func Test_ExternalRefs(t *testing.T) {
{
name: "cpe + purl",
input: pkg.Package{
CPEs: []pkg.CPE{
CPEs: []cpe.CPE{
testCPE,
},
PURL: "a-purl",
@@ -26,7 +27,7 @@ func Test_ExternalRefs(t *testing.T) {
expected: []ExternalRef{
{
ReferenceCategory: SecurityReferenceCategory,
ReferenceLocator: pkg.CPEString(testCPE),
ReferenceLocator: cpe.String(testCPE),
ReferenceType: Cpe23ExternalRefType,
},
{
@@ -8,6 +8,11 @@ const (
// Example: The package 'WildFly' is described by SPDX document WildFly.spdx.
DescribedByRelationship RelationshipType = "DESCRIBED_BY"
// Describes is to be used when SPDXRef-DOCUMENT describes SPDXRef-A.
// Example: An SPDX document WildFly.spdx describes package ‘WildFly’.
// Note this is a logical relationship to help organize related items within an SPDX document that is mandatory if more than one package or set of files (not in a package) is present.
DescribesRelationship RelationshipType = "DESCRIBES"
// ContainsRelationship is to be used when SPDXRef-A contains SPDXRef-B.
// Example: An ARCHIVE file bar.tgz contains a SOURCE file foo.c.
ContainsRelationship RelationshipType = "CONTAINS"
@@ -43,6 +43,8 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from portage DB"
case pkg.HackagePkg:
answer = "acquired package info from cabal or stack manifest files"
case pkg.HexPkg:
answer = "acquired package info from rebar3 or mix manifest file"
default:
answer = "acquired package info from the following paths"
}
@@ -191,6 +191,14 @@ func Test_SourceInfo(t *testing.T) {
"acquired package info from the following paths",
},
},
{
input: pkg.Package{
Type: pkg.HexPkg,
},
expected: []string{
"from rebar3 or mix manifest file",
},
},
}
var pkgTypes []pkg.Type
for _, test := range tests {
@@ -1,6 +1,8 @@
//nolint:gosec // sha1 is used as a required hash function for SPDX, not a crypto function
package spdxhelpers
import (
"crypto/sha1"
"fmt"
"sort"
"strings"
@@ -31,6 +33,24 @@ const (
//nolint:funlen
func ToFormatModel(s sbom.SBOM) *spdx.Document {
name, namespace := DocumentNameAndNamespace(s.Source)
relationships := toRelationships(s.RelationshipsSorted())
// for valid SPDX we need a document describes relationship
// TODO: remove this placeholder after deciding on correct behavior
// for the primary package purpose field:
// https://spdx.github.io/spdx-spec/v2.3/package-information/#724-primary-package-purpose-field
documentDescribesRelationship := &spdx.Relationship{
RefA: common.DocElementID{
ElementRefID: "DOCUMENT",
},
Relationship: string(DescribesRelationship),
RefB: common.DocElementID{
ElementRefID: "DOCUMENT",
},
RelationshipComment: "",
}
relationships = append(relationships, documentDescribesRelationship)
return &spdx.Document{
// 6.1: SPDX Version; should be in the format "SPDX-x.x"
@@ -103,9 +123,9 @@ func ToFormatModel(s sbom.SBOM) *spdx.Document {
// Cardinality: optional, one
CreatorComment: "",
},
Packages: toPackages(s.Artifacts.PackageCatalog),
Packages: toPackages(s.Artifacts.PackageCatalog, s),
Files: toFiles(s),
Relationships: toRelationships(s.RelationshipsSorted()),
Relationships: relationships,
}
}
@@ -123,7 +143,7 @@ func toSPDXID(identifiable artifact.Identifiable) common.ElementID {
// packages populates all Package Information from the package Catalog (see https://spdx.github.io/spdx-spec/3-package-information/)
//
//nolint:funlen
func toPackages(catalog *pkg.Catalog) (results []*spdx.Package) {
func toPackages(catalog *pkg.Catalog, sbom sbom.SBOM) (results []*spdx.Package) {
for _, p := range catalog.Sorted() {
// name should be guaranteed to be unique, but semantically useful and stable
id := toSPDXID(p)
@@ -132,7 +152,25 @@ func toPackages(catalog *pkg.Catalog) (results []*spdx.Package) {
// in the Comments on License field (section 7.16). With respect to NOASSERTION, a written explanation in
// the Comments on License field (section 7.16) is preferred.
license := License(p)
checksums, filesAnalyzed := toPackageChecksums(p)
// two ways to get filesAnalyzed == true:
// 1. syft has generated a sha1 digest for the package itself - usually in the java cataloger
// 2. syft has generated a sha1 digest for the package's contents
packageChecksums, filesAnalyzed := toPackageChecksums(p)
packageVerificationCode := newPackageVerificationCode(p, sbom)
if packageVerificationCode != nil {
filesAnalyzed = true
}
// invalid SPDX document state
if filesAnalyzed && packageVerificationCode == nil {
// this is an invalid document state
// we reset the filesAnalyzed flag to false to avoid
// cases where a package digest was generated but there was
// not enough metadata to generate a verification code regarding the files
filesAnalyzed = false
}
results = append(results, &spdx.Package{
// NOT PART OF SPEC
@@ -193,7 +231,7 @@ func toPackages(catalog *pkg.Catalog) (results []*spdx.Package) {
// 7.9: Package Verification Code
// Cardinality: optional, one if filesAnalyzed is true / omitted;
// zero (must be omitted) if filesAnalyzed is false
PackageVerificationCode: nil,
PackageVerificationCode: packageVerificationCode,
// 7.10: Package Checksum: may have keys for SHA1, SHA256 and/or MD5
// Cardinality: optional, one or many
@@ -203,7 +241,7 @@ func toPackages(catalog *pkg.Catalog) (results []*spdx.Package) {
// to determine if any file in the original package has been changed. If the SPDX file is to be included
// in a package, this value should not be calculated. The SHA-1 algorithm will be used to provide the
// checksum by default.
PackageChecksums: checksums,
PackageChecksums: packageChecksums,
// 7.11: Package Home Page
// Cardinality: optional, one
@@ -275,17 +313,6 @@ func toPackages(catalog *pkg.Catalog) (results []*spdx.Package) {
return results
}
func toPackageOriginator(p pkg.Package) *common.Originator {
kind, originator := Originator(p)
if kind == "" || originator == "" {
return nil
}
return &common.Originator{
Originator: originator,
OriginatorType: kind,
}
}
func toPackageChecksums(p pkg.Package) ([]common.Checksum, bool) {
filesAnalyzed := false
var checksums []common.Checksum
@@ -293,6 +320,7 @@ func toPackageChecksums(p pkg.Package) ([]common.Checksum, bool) {
// we generate digest for some Java packages
// spdx.github.io/spdx-spec/package-information/#710-package-checksum-field
case pkg.JavaMetadata:
// if syft has generated the digest here then filesAnalyzed is true
if len(meta.ArchiveDigests) > 0 {
filesAnalyzed = true
for _, digest := range meta.ArchiveDigests {
@@ -303,7 +331,8 @@ func toPackageChecksums(p pkg.Package) ([]common.Checksum, bool) {
})
}
}
case pkg.GolangBinMetadata:
case pkg.GolangMetadata:
// because the H1 digest is found in the Golang metadata we cannot claim that the files were analyzed
algo, hexStr, err := util.HDigestToSHA(meta.H1Digest)
if err != nil {
log.Debugf("invalid h1digest: %s: %v", meta.H1Digest, err)
@@ -318,6 +347,17 @@ func toPackageChecksums(p pkg.Package) ([]common.Checksum, bool) {
return checksums, filesAnalyzed
}
func toPackageOriginator(p pkg.Package) *common.Originator {
kind, originator := Originator(p)
if kind == "" || originator == "" {
return nil
}
return &common.Originator{
Originator: originator,
OriginatorType: kind,
}
}
func formatSPDXExternalRefs(p pkg.Package) (refs []*spdx.PackageExternalReference) {
for _, ref := range ExternalRefs(p) {
refs = append(refs, &spdx.PackageExternalReference{
@@ -385,6 +425,15 @@ func toFiles(s sbom.SBOM) (results []*spdx.File) {
digests = digestsForLocation
}
// if we don't have any metadata or digests for this location
// then the file is most likely a symlink or non-regular file
// for now we include a 0 sha1 digest as requested by the spdx spec
// TODO: update location code in core SBOM so that we can map complex links
// back to their real file digest location.
if len(digests) == 0 {
digests = append(digests, file.Digest{Algorithm: "sha1", Value: "0000000000000000000000000000000000000000"})
}
// TODO: add file classifications (?) and content as a snippet
var comment string
@@ -414,6 +463,7 @@ func toFiles(s sbom.SBOM) (results []*spdx.File) {
}
func toFileChecksums(digests []file.Digest) (checksums []common.Checksum) {
checksums = make([]common.Checksum, 0, len(digests))
for _, digest := range digests {
checksums = append(checksums, common.Checksum{
Algorithm: toChecksumAlgorithm(digest.Algorithm),
@@ -462,3 +512,55 @@ func toFileTypes(metadata *source.FileMetadata) (ty []string) {
return ty
}
// TODO: handle SPDX excludes file case
// f file is an "excludes" file, skip it /* exclude SPDX analysis file(s) */
// see: https://spdx.github.io/spdx-spec/v2.3/package-information/#79-package-verification-code-field
// the above link contains the SPDX algorithm for a package verification code
func newPackageVerificationCode(p pkg.Package, sbom sbom.SBOM) *common.PackageVerificationCode {
// key off of the contains relationship;
// spdx validator will fail if a package claims to contain a file but no sha1 provided
// if a sha1 for a file is provided then the validator will fail if the package does not have
// a package verification code
coordinates := sbom.CoordinatesForPackage(p, artifact.ContainsRelationship)
var digests []file.Digest
for _, c := range coordinates {
digest := sbom.Artifacts.FileDigests[c]
if len(digest) == 0 {
continue
}
var d file.Digest
for _, digest := range digest {
if digest.Algorithm == "sha1" {
d = digest
break
}
}
digests = append(digests, d)
}
if len(digests) == 0 {
return nil
}
// sort templist in ascending order by SHA1 value
sort.SliceStable(digests, func(i, j int) bool {
return digests[i].Value < digests[j].Value
})
// filelist = templist with "/n"s removed. /* ordered sequence of SHA1 values with no separators
var b strings.Builder
for _, digest := range digests {
b.WriteString(digest.Value)
}
//nolint:gosec
hasher := sha1.New()
_, _ = hasher.Write([]byte(b.String()))
return &common.PackageVerificationCode{
// 7.9.1: Package Verification Code Value
// Cardinality: mandatory, one
Value: fmt.Sprintf("%+x", hasher.Sum(nil)),
}
}
@@ -12,6 +12,7 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
"github.com/anchore/syft/syft/source"
)
@@ -75,8 +76,8 @@ func Test_toPackageChecksums(t *testing.T) {
Name: "test",
Version: "1.0.0",
Language: pkg.Go,
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
MetadataType: pkg.GolangMetadataType,
Metadata: pkg.GolangMetadata{
H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
},
},
@@ -369,6 +370,7 @@ func Test_fileIDsForPackage(t *testing.T) {
}
func Test_H1Digest(t *testing.T) {
sbom := sbom.SBOM{}
tests := []struct {
name string
pkg pkg.Package
@@ -379,8 +381,8 @@ func Test_H1Digest(t *testing.T) {
pkg: pkg.Package{
Name: "github.com/googleapis/gnostic",
Version: "v0.5.5",
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
MetadataType: pkg.GolangMetadataType,
Metadata: pkg.GolangMetadata{
H1Digest: "h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
},
},
@@ -391,8 +393,8 @@ func Test_H1Digest(t *testing.T) {
pkg: pkg.Package{
Name: "github.com/googleapis/gnostic",
Version: "v0.5.5",
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
MetadataType: pkg.GolangMetadataType,
Metadata: pkg.GolangMetadata{
H1Digest: "h1:9fHAtK0uzzz",
},
},
@@ -403,8 +405,8 @@ func Test_H1Digest(t *testing.T) {
pkg: pkg.Package{
Name: "github.com/googleapis/gnostic",
Version: "v0.5.5",
MetadataType: pkg.GolangBinMetadataType,
Metadata: pkg.GolangBinMetadata{
MetadataType: pkg.GolangMetadataType,
Metadata: pkg.GolangMetadata{
H1Digest: "h12:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=",
},
},
@@ -415,7 +417,7 @@ func Test_H1Digest(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
catalog := pkg.NewCatalog(test.pkg)
pkgs := toPackages(catalog)
pkgs := toPackages(catalog, sbom)
require.Len(t, pkgs, 1)
for _, p := range pkgs {
if test.expectedDigest == "" {
@@ -11,6 +11,7 @@ import (
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/formats/common/util"
"github.com/anchore/syft/syft/linux"
@@ -365,7 +366,7 @@ func extractMetadata(p *spdx.Package, info pkgInfo) (pkg.MetadataType, interface
h1Digest = digest
break
}
return pkg.GolangBinMetadataType, pkg.GolangBinMetadata{
return pkg.GolangMetadataType, pkg.GolangMetadata{
H1Digest: h1Digest,
}
}
@@ -381,15 +382,15 @@ func findPURLValue(p *spdx.Package) string {
return ""
}
func extractCPEs(p *spdx.Package) (cpes []pkg.CPE) {
func extractCPEs(p *spdx.Package) (cpes []cpe.CPE) {
for _, r := range p.PackageExternalReferences {
if r.RefType == string(Cpe23ExternalRefType) {
cpe, err := pkg.NewCPE(r.Locator)
c, err := cpe.New(r.Locator)
if err != nil {
log.Warnf("unable to extract SPDX CPE=%q: %+v", r.Locator, err)
continue
}
cpes = append(cpes, cpe)
cpes = append(cpes, c)
}
}
return cpes
@@ -46,7 +46,7 @@ func TestToSyftModel(t *testing.T) {
},
{
Category: "PACKAGE-MANAGER",
Locator: "pkg:alpine/pkg-1@5.4.3?arch=x86_64&upstream=p1-origin&distro=alpine-3.10.9",
Locator: "pkg:apk/alpine/pkg-1@5.4.3?arch=x86_64&upstream=p1-origin&distro=alpine-3.10.9",
RefType: "purl",
},
},
@@ -145,7 +145,7 @@ func Test_extractMetadata(t *testing.T) {
PackageExternalReferences: []*spdx.PackageExternalReference{
{
Category: "PACKAGE-MANAGER",
Locator: "pkg:alpine/pkg-2@7.3.1?arch=x86_64&upstream=apk-origin@9.1.3&distro=alpine-3.10.9",
Locator: "pkg:apk/alpine/pkg-2@7.3.1?arch=x86_64&upstream=apk-origin@9.1.3&distro=alpine-3.10.9",
RefType: "purl",
},
},
@@ -302,8 +302,8 @@ func TestH1Digest(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
p := toSyftPackage(&test.pkg)
require.Equal(t, pkg.GolangBinMetadataType, p.MetadataType)
meta := p.Metadata.(pkg.GolangBinMetadata)
require.Equal(t, pkg.GolangMetadataType, p.MetadataType)
meta := p.Metadata.(pkg.GolangMetadata)
require.Equal(t, test.expectedDigest, meta.H1Digest)
})
}
+19 -13
View File
@@ -9,12 +9,14 @@ import (
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/go-testutils"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/sbom"
@@ -35,7 +37,7 @@ func FromSnapshot() ImageOption {
}
}
func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, testImage string, updateSnapshot bool, redactors ...redactor) {
func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, testImage string, updateSnapshot bool, json bool, redactors ...redactor) {
var buffer bytes.Buffer
// grab the latest image contents and persist
@@ -61,15 +63,17 @@ func AssertEncoderAgainstGoldenImageSnapshot(t *testing.T, format sbom.Format, s
expected = r(expected)
}
// assert that the golden file snapshot matches the actual contents
if !bytes.Equal(expected, actual) {
if json {
require.JSONEq(t, string(expected), string(actual))
} else if !bytes.Equal(expected, actual) {
// assert that the golden file snapshot matches the actual contents
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}
func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, updateSnapshot bool, redactors ...redactor) {
func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format sbom.Format, sbom sbom.SBOM, updateSnapshot bool, json bool, redactors ...redactor) {
var buffer bytes.Buffer
err := format.Encode(&buffer, sbom)
@@ -90,7 +94,9 @@ func AssertEncoderAgainstGoldenSnapshot(t *testing.T, format sbom.Format, sbom s
expected = r(expected)
}
if !bytes.Equal(expected, actual) {
if json {
require.JSONEq(t, string(expected), string(actual))
} else if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Logf("len: %d\nexpected: %s", len(expected), expected)
@@ -174,8 +180,8 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
Version: "1.0.1",
},
PURL: "a-purl-1", // intentionally a bad pURL for test fixtures
CPEs: []pkg.CPE{
pkg.MustCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
},
})
catalog.Add(pkg.Package{
@@ -192,8 +198,8 @@ func populateImageCatalog(catalog *pkg.Catalog, img *image.Image) {
Version: "2.0.1",
},
PURL: "pkg:deb/debian/package-2@2.0.1",
CPEs: []pkg.CPE{
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
},
})
}
@@ -254,8 +260,8 @@ func newDirectoryCatalog() *pkg.Catalog {
},
},
PURL: "a-purl-2", // intentionally a bad pURL for test fixtures
CPEs: []pkg.CPE{
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
},
})
catalog.Add(pkg.Package{
@@ -272,8 +278,8 @@ func newDirectoryCatalog() *pkg.Catalog {
Version: "2.0.1",
},
PURL: "pkg:deb/debian/package-2@2.0.1",
CPEs: []pkg.CPE{
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
},
})
+1 -1
View File
@@ -13,7 +13,7 @@ func encoder(output io.Writer, s sbom.SBOM) error {
bom := cyclonedxhelpers.ToFormatModel(s)
enc := cyclonedx.NewBOMEncoder(output, cyclonedx.BOMFileFormatJSON)
enc.SetPretty(true)
enc.SetEscapeHTML(false)
err := enc.Encode(bom)
return err
}
+5 -3
View File
@@ -15,6 +15,7 @@ func TestCycloneDxDirectoryEncoder(t *testing.T) {
Format(),
testutils.DirectoryInput(t),
*updateCycloneDx,
true,
cycloneDxRedactor,
)
}
@@ -26,19 +27,20 @@ func TestCycloneDxImageEncoder(t *testing.T) {
testutils.ImageInput(t, testImage),
testImage,
*updateCycloneDx,
true,
cycloneDxRedactor,
)
}
func cycloneDxRedactor(s []byte) []byte {
serialPattern := regexp.MustCompile(`urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)
rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`)
rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`)
sha256Pattern := regexp.MustCompile(`sha256:[A-Fa-f0-9]{64}`)
for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern, sha256Pattern} {
s = pattern.ReplaceAll(s, []byte("redacted"))
s = pattern.ReplaceAll(s, []byte(""))
}
// the bom-ref will be autogenerated every time, the value here should not be directly tested in snapshot tests
s = regexp.MustCompile(` "bom-ref": .*\n`).ReplaceAll(s, []byte(""))
s = regexp.MustCompile(`\s+"bom-ref":\s*"[^"]+",?\n`).ReplaceAll(s, []byte(""))
return s
}
@@ -1,10 +1,10 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:cfd602eb-022b-4a61-84d4-50d34612bd84",
"serialNumber": "urn:uuid:0b628da1-274e-4c24-821c-f9452f37db54",
"version": 1,
"metadata": {
"timestamp": "2022-11-07T09:11:21-05:00",
"timestamp": "2022-12-22T18:33:51-05:00",
"tools": [
{
"vendor": "anchore",
@@ -1,10 +1,10 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:ced49687-6384-48fa-a930-ef83a258bf77",
"serialNumber": "urn:uuid:542fc1a1-81ac-4b76-b9e2-8e6b9d8c840a",
"version": 1,
"metadata": {
"timestamp": "2022-11-07T09:11:21-05:00",
"timestamp": "2022-12-22T18:33:51-05:00",
"tools": [
{
"vendor": "anchore",
@@ -13,7 +13,7 @@
}
],
"component": {
"bom-ref": "522dc6b135a55bb4",
"bom-ref": "ffd645a093c0fe70",
"type": "container",
"name": "user-image-input",
"version": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
@@ -53,7 +53,7 @@
},
{
"name": "syft:location:0:layerID",
"value": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59"
"value": "sha256:62058900d4ce269c900160b8dd255fe310c3a459dda236d041102fa070f84406"
},
{
"name": "syft:location:0:path",
@@ -83,7 +83,7 @@
},
{
"name": "syft:location:0:layerID",
"value": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec"
"value": "sha256:623ad97366f39ae279f1925673cdacb4851ddf2e3266f04e63010ec080a098c1"
},
{
"name": "syft:location:0:path",
@@ -15,6 +15,7 @@ func TestCycloneDxDirectoryEncoder(t *testing.T) {
Format(),
testutils.DirectoryInput(t),
*updateCycloneDx,
false,
cycloneDxRedactor,
)
}
@@ -26,6 +27,7 @@ func TestCycloneDxImageEncoder(t *testing.T) {
testutils.ImageInput(t, testImage),
testImage,
*updateCycloneDx,
false,
cycloneDxRedactor,
)
}
+6 -3
View File
@@ -15,6 +15,7 @@ func TestSPDXJSONDirectoryEncoder(t *testing.T) {
Format(),
testutils.DirectoryInput(t),
*updateSpdxJson,
true,
spdxJsonRedactor,
)
}
@@ -26,6 +27,7 @@ func TestSPDXJSONImageEncoder(t *testing.T) {
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
testImage,
*updateSpdxJson,
true,
spdxJsonRedactor,
)
}
@@ -39,17 +41,18 @@ func TestSPDXRelationshipOrder(t *testing.T) {
s,
testImage,
*updateSpdxJson,
true,
spdxJsonRedactor,
)
}
func spdxJsonRedactor(s []byte) []byte {
// each SBOM reports the time it was generated, which is not useful during snapshot testing
s = regexp.MustCompile(`"created": .*`).ReplaceAll(s, []byte("redacted"))
s = regexp.MustCompile(`"created":\s+"[^"]*",?`).ReplaceAll(s, []byte(""))
// each SBOM reports a unique documentNamespace when generated, this is not useful for snapshot testing
s = regexp.MustCompile(`"documentNamespace": .*`).ReplaceAll(s, []byte("redacted"))
s = regexp.MustCompile(`"documentNamespace":\s+"[^"]*",?`).ReplaceAll(s, []byte(""))
// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
return regexp.MustCompile(`"licenseListVersion": .*`).ReplaceAll(s, []byte("redacted"))
return regexp.MustCompile(`"licenseListVersion":\s+"[^"]*",?`).ReplaceAll(s, []byte(""))
}
@@ -3,14 +3,14 @@
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "/some/path",
"documentNamespace": "https://anchore.com/syft/dir/some/path-4bf54cdd-0a0f-4560-bf4f-39cac2ef7a5b",
"documentNamespace": "https://anchore.com/syft/dir/some/path-e13c8924-4bbc-42f8-bd30-4e1554472d62",
"creationInfo": {
"licenseListVersion": "3.18",
"licenseListVersion": "3.19",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-v0.42.0-bogus"
],
"created": "2022-11-19T13:46:57Z",
"created": "2022-12-22T23:33:52Z",
"comment": ""
},
"packages": [
@@ -62,5 +62,12 @@
}
]
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-DOCUMENT",
"relationshipType": "DESCRIBES"
}
]
}
@@ -3,14 +3,14 @@
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "user-image-input",
"documentNamespace": "https://anchore.com/syft/image/user-image-input-102ca7dc-3d1e-46d2-b130-28968831ebcc",
"documentNamespace": "https://anchore.com/syft/image/user-image-input-a1cc9d58-830a-4a4b-9dcd-f41ea3001216",
"creationInfo": {
"licenseListVersion": "3.18",
"licenseListVersion": "3.19",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-v0.42.0-bogus"
],
"created": "2022-11-19T13:46:57Z",
"created": "2022-12-22T23:33:53Z",
"comment": ""
},
"packages": [
@@ -62,5 +62,12 @@
}
]
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-DOCUMENT",
"relationshipType": "DESCRIBES"
}
]
}
@@ -3,14 +3,14 @@
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "user-image-input",
"documentNamespace": "https://anchore.com/syft/image/user-image-input-55ad4afc-ecdc-46a4-8bc3-36b3e72da5d1",
"documentNamespace": "https://anchore.com/syft/image/user-image-input-fc663ee3-0f9b-402e-827f-3f29aeff164e",
"creationInfo": {
"licenseListVersion": "3.18",
"licenseListVersion": "3.19",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-v0.42.0-bogus"
],
"created": "2022-11-19T13:46:57Z",
"created": "2022-12-22T23:33:53Z",
"comment": ""
},
"packages": [
@@ -70,7 +70,12 @@
"fileTypes": [
"OTHER"
],
"checksums": null,
"checksums": [
{
"algorithm": "SHA1",
"checksumValue": "0000000000000000000000000000000000000000"
}
],
"licenseConcluded": "NOASSERTION",
"copyrightText": ""
},
@@ -80,7 +85,12 @@
"fileTypes": [
"OTHER"
],
"checksums": null,
"checksums": [
{
"algorithm": "SHA1",
"checksumValue": "0000000000000000000000000000000000000000"
}
],
"licenseConcluded": "NOASSERTION",
"copyrightText": ""
},
@@ -90,7 +100,12 @@
"fileTypes": [
"OTHER"
],
"checksums": null,
"checksums": [
{
"algorithm": "SHA1",
"checksumValue": "0000000000000000000000000000000000000000"
}
],
"licenseConcluded": "NOASSERTION",
"copyrightText": ""
},
@@ -100,7 +115,12 @@
"fileTypes": [
"OTHER"
],
"checksums": null,
"checksums": [
{
"algorithm": "SHA1",
"checksumValue": "0000000000000000000000000000000000000000"
}
],
"licenseConcluded": "NOASSERTION",
"copyrightText": ""
},
@@ -110,7 +130,12 @@
"fileTypes": [
"OTHER"
],
"checksums": null,
"checksums": [
{
"algorithm": "SHA1",
"checksumValue": "0000000000000000000000000000000000000000"
}
],
"licenseConcluded": "NOASSERTION",
"copyrightText": ""
},
@@ -120,7 +145,12 @@
"fileTypes": [
"OTHER"
],
"checksums": null,
"checksums": [
{
"algorithm": "SHA1",
"checksumValue": "0000000000000000000000000000000000000000"
}
],
"licenseConcluded": "NOASSERTION",
"copyrightText": ""
}
@@ -155,6 +185,11 @@
"spdxElementId": "SPDXRef-Package-python-package-1-66ba429119b8bec6",
"relatedSpdxElement": "SPDXRef-f9e49132a4b96ccd",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-DOCUMENT",
"relationshipType": "DESCRIBES"
}
]
}
@@ -27,7 +27,7 @@
},
{
"referenceCategory": "PACKAGE_MANAGER",
"referenceLocator": "pkg:alpine/busybox@1.30.1-r5?arch=x86_64&distro=alpine-3.10.9",
"referenceLocator": "pkg:apk/alpine/busybox@1.30.1-r5?arch=x86_64&distro=alpine-3.10.9",
"referenceType": "purl"
}
],
@@ -51,7 +51,7 @@
},
{
"referenceCategory": "PACKAGE_MANAGER",
"referenceLocator": "pkg:alpine/libssl1.1@1.1.1k-r0?arch=x86_64&distro=alpine-3.10.9",
"referenceLocator": "pkg:apk/alpine/libssl1.1@1.1.1k-r0?arch=x86_64&distro=alpine-3.10.9",
"referenceType": "purl"
}
],
@@ -100,7 +100,7 @@
},
{
"referenceCategory": "PACKAGE_MANAGER",
"referenceLocator": "pkg:alpine/ssl_client@1.30.1-r5?arch=x86_64&upstream=busybox&distro=alpine-3.10.9",
"referenceLocator": "pkg:apk/alpine/ssl_client@1.30.1-r5?arch=x86_64&upstream=busybox&distro=alpine-3.10.9",
"referenceType": "purl"
}
],
@@ -19,6 +19,7 @@ func TestSPDXTagValueDirectoryEncoder(t *testing.T) {
Format(),
testutils.DirectoryInput(t),
*updateSpdxTagValue,
false,
spdxTagValueRedactor,
)
}
@@ -30,6 +31,7 @@ func TestSPDXTagValueImageEncoder(t *testing.T) {
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
testImage,
*updateSpdxTagValue,
false,
spdxTagValueRedactor,
)
}
@@ -63,6 +65,7 @@ func TestSPDXJSONSPDXIDs(t *testing.T) {
},
},
*updateSpdxTagValue,
false,
spdxTagValueRedactor,
)
}
@@ -76,6 +79,7 @@ func TestSPDXRelationshipOrder(t *testing.T) {
s,
testImage,
*updateSpdxTagValue,
false,
spdxTagValueRedactor,
)
}
@@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentName: foobar/baz
DocumentNamespace: https://anchore.com/syft/dir/foobar/baz-62bc0aae-2b37-4c86-ab79-63c6fc4198ed
LicenseListVersion: 3.18
DocumentNamespace: https://anchore.com/syft/dir/foobar/baz-478e410d-7fad-472c-b4e9-a4068ef28160
LicenseListVersion: 3.19
Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus
Created: 2022-11-19T13:48:30Z
Created: 2022-12-21T03:39:05Z
##### Package: @at-sign
@@ -41,3 +41,7 @@ PackageLicenseConcluded: NONE
PackageLicenseDeclared: NONE
PackageCopyrightText: NOASSERTION
##### Relationships
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DOCUMENT
@@ -2,42 +2,48 @@ SPDXVersion: SPDX-2.3
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentName: user-image-input
DocumentNamespace: https://anchore.com/syft/image/user-image-input-cc20e416-9c74-401c-b4aa-245556bada5e
LicenseListVersion: 3.18
DocumentNamespace: https://anchore.com/syft/image/user-image-input-73433e8c-364f-42b6-b5b7-9a4da8799868
LicenseListVersion: 3.19
Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus
Created: 2022-11-19T13:48:30Z
Created: 2022-12-21T03:39:05Z
##### Unpackaged files
FileName: /f1
SPDXID: SPDXRef-5265a4dde3edbf7c
FileType: OTHER
FileChecksum: SHA1: 0000000000000000000000000000000000000000
LicenseConcluded: NOASSERTION
FileName: /z1/f5
SPDXID: SPDXRef-839d99ee67d9d174
FileType: OTHER
FileChecksum: SHA1: 0000000000000000000000000000000000000000
LicenseConcluded: NOASSERTION
FileName: /a1/f6
SPDXID: SPDXRef-9c2f7510199b17f6
FileType: OTHER
FileChecksum: SHA1: 0000000000000000000000000000000000000000
LicenseConcluded: NOASSERTION
FileName: /d2/f4
SPDXID: SPDXRef-c641caa71518099f
FileType: OTHER
FileChecksum: SHA1: 0000000000000000000000000000000000000000
LicenseConcluded: NOASSERTION
FileName: /d1/f3
SPDXID: SPDXRef-c6f5b29dca12661f
FileType: OTHER
FileChecksum: SHA1: 0000000000000000000000000000000000000000
LicenseConcluded: NOASSERTION
FileName: /f2
SPDXID: SPDXRef-f9e49132a4b96ccd
FileType: OTHER
FileChecksum: SHA1: 0000000000000000000000000000000000000000
LicenseConcluded: NOASSERTION
##### Package: package-2
@@ -76,4 +82,5 @@ Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef
Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-c641caa71518099f
Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-c6f5b29dca12661f
Relationship: SPDXRef-Package-python-package-1-66ba429119b8bec6 CONTAINS SPDXRef-f9e49132a4b96ccd
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DOCUMENT
@@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentName: /some/path
DocumentNamespace: https://anchore.com/syft/dir/some/path-7a4b2140-6669-4a28-80dd-5c8e795c5da0
LicenseListVersion: 3.18
DocumentNamespace: https://anchore.com/syft/dir/some/path-1d303762-46d2-47b5-9c81-defa91387275
LicenseListVersion: 3.19
Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus
Created: 2022-11-19T13:48:30Z
Created: 2022-12-21T03:39:05Z
##### Package: package-2
@@ -36,3 +36,7 @@ PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
ExternalRef: PACKAGE-MANAGER purl a-purl-2
##### Relationships
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DOCUMENT
@@ -2,11 +2,11 @@ SPDXVersion: SPDX-2.3
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentName: user-image-input
DocumentNamespace: https://anchore.com/syft/image/user-image-input-baff7ada-85cb-403e-90d7-05b0c6d79490
LicenseListVersion: 3.18
DocumentNamespace: https://anchore.com/syft/image/user-image-input-559af225-63af-4bc0-94fb-bce94913bcfa
LicenseListVersion: 3.19
Creator: Organization: Anchore, Inc
Creator: Tool: syft-v0.42.0-bogus
Created: 2022-11-19T13:48:30Z
Created: 2022-12-21T03:39:05Z
##### Package: package-2
@@ -36,3 +36,7 @@ PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:1:*:*:*:*:*:*:*
ExternalRef: PACKAGE-MANAGER purl a-purl-1
##### Relationships
Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-DOCUMENT
@@ -21,7 +21,7 @@ PackageLicenseDeclared: GPL-2.0-only
PackageCopyrightText: NOASSERTION
PackageDescription: Size optimized toolbox of many common UNIX utilities
ExternalRef: SECURITY cpe23Type cpe:2.3:a:busybox:busybox:1.31.1-r19:*:*:*:*:*:*:*
ExternalRef: PACKAGE-MANAGER purl pkg:alpine/busybox@1.31.1-r19?arch=x86_64&upstream=busybox&distro=alpine-3.12.5
ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/busybox@1.31.1-r19?arch=x86_64&upstream=busybox&distro=alpine-3.12.5
##### Package: my-app
+8 -4
View File
@@ -5,6 +5,7 @@ import (
"testing"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/formats/common/testutils"
"github.com/anchore/syft/syft/linux"
@@ -19,6 +20,7 @@ func TestDirectoryEncoder(t *testing.T) {
testutils.AssertEncoderAgainstGoldenSnapshot(t,
Format(),
testutils.DirectoryInput(t),
true,
*updateJson,
)
}
@@ -29,6 +31,7 @@ func TestImageEncoder(t *testing.T) {
Format(),
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
testImage,
true,
*updateJson,
)
}
@@ -57,8 +60,8 @@ func TestEncodeFullJSONDocument(t *testing.T) {
Files: []pkg.PythonFileRecord{},
},
PURL: "a-purl-1",
CPEs: []pkg.CPE{
pkg.MustCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"),
},
}
@@ -81,8 +84,8 @@ func TestEncodeFullJSONDocument(t *testing.T) {
Files: []pkg.DpkgFileRecord{},
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
pkg.MustCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"),
},
}
@@ -199,5 +202,6 @@ func TestEncodeFullJSONDocument(t *testing.T) {
Format(),
s,
*updateJson,
true,
)
}
+5 -6
View File
@@ -6,12 +6,11 @@ import (
)
type File struct {
ID string `json:"id"`
Location source.Coordinates `json:"location"`
Metadata *FileMetadataEntry `json:"metadata,omitempty"`
Contents string `json:"contents,omitempty"`
Digests []file.Digest `json:"digests,omitempty"`
Classifications []file.Classification `json:"classifications,omitempty"`
ID string `json:"id"`
Location source.Coordinates `json:"location"`
Metadata *FileMetadataEntry `json:"metadata,omitempty"`
Contents string `json:"contents,omitempty"`
Digests []file.Digest `json:"digests,omitempty"`
}
type FileMetadataEntry struct {
+14 -6
View File
@@ -34,7 +34,7 @@ func TestUnmarshalPackageGolang(t *testing.T) {
"language": "go",
"cpes": [],
"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0",
"metadataType": "GolangBinMetadata",
"metadataType": "GolangMetadata",
"metadata": {
"goCompiledVersion": "go1.18",
"architecture": "amd64",
@@ -43,7 +43,7 @@ func TestUnmarshalPackageGolang(t *testing.T) {
}`),
assert: func(p *Package) {
assert.NotNil(t, p.Metadata)
golangMetadata := p.Metadata.(pkg.GolangBinMetadata)
golangMetadata := p.Metadata.(pkg.GolangMetadata)
assert.NotEmpty(t, golangMetadata)
assert.Equal(t, "go1.18", golangMetadata.GoCompiledVersion)
},
@@ -93,7 +93,7 @@ func Test_unpackMetadata(t *testing.T) {
}{
{
name: "unmarshal package metadata",
metadataType: pkg.GolangBinMetadataType,
metadataType: pkg.GolangMetadataType,
packageData: []byte(`{
"id": "8b594519bc23da50",
"name": "gopkg.in/square/go-jose.v2",
@@ -109,7 +109,7 @@ func Test_unpackMetadata(t *testing.T) {
"language": "go",
"cpes": [],
"purl": "pkg:golang/gopkg.in/square/go-jose.v2@v2.6.0",
"metadataType": "GolangBinMetadata",
"metadataType": "GolangMetadata",
"metadata": {
"goCompiledVersion": "go1.18",
"architecture": "amd64",
@@ -214,11 +214,19 @@ func Test_unpackMetadata(t *testing.T) {
},
{
name: "can handle package with metadata type but missing metadata",
packageData: []byte(`{
"metadataType": "GolangMetadata"
}`),
metadataType: pkg.GolangMetadataType,
wantMetadata: pkg.GolangMetadata{},
},
{
name: "can handle package with golang bin metadata type",
packageData: []byte(`{
"metadataType": "GolangBinMetadata"
}`),
metadataType: pkg.GolangBinMetadataType,
wantMetadata: pkg.GolangBinMetadata{},
metadataType: pkg.GolangMetadataType,
wantMetadata: pkg.GolangMetadata{},
},
{
name: "can handle package with unknonwn metadata type and missing metadata",
@@ -185,7 +185,7 @@
}
},
"schema": {
"version": "6.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-6.0.0.json"
"version": "6.1.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-6.1.0.json"
}
}
+7 -12
View File
@@ -8,6 +8,7 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/formats/syftjson/model"
"github.com/anchore/syft/syft/linux"
@@ -102,23 +103,17 @@ func toFile(s sbom.SBOM) []model.File {
digests = digestsForLocation
}
var classifications []file.Classification
if classificationsForLocation, exists := artifacts.FileClassifications[coordinates]; exists {
classifications = classificationsForLocation
}
var contents string
if contentsForLocation, exists := artifacts.FileContents[coordinates]; exists {
contents = contentsForLocation
}
results = append(results, model.File{
ID: string(coordinates.ID()),
Location: coordinates,
Metadata: toFileMetadataEntry(coordinates, metadata),
Digests: digests,
Classifications: classifications,
Contents: contents,
ID: string(coordinates.ID()),
Location: coordinates,
Metadata: toFileMetadataEntry(coordinates, metadata),
Digests: digests,
Contents: contents,
})
}
@@ -165,7 +160,7 @@ func toPackageModels(catalog *pkg.Catalog) []model.Package {
func toPackageModel(p pkg.Package) model.Package {
var cpes = make([]string, len(p.CPEs))
for i, c := range p.CPEs {
cpes[i] = pkg.CPEString(c)
cpes[i] = cpe.String(c)
}
var licenses = make([]string, 0)
+3 -2
View File
@@ -7,6 +7,7 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/formats/syftjson/model"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
@@ -174,9 +175,9 @@ func toSyftCatalog(pkgs []model.Package, idAliases map[string]string) *pkg.Catal
}
func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package {
var cpes []pkg.CPE
var cpes []cpe.CPE
for _, c := range p.CPEs {
value, err := pkg.NewCPE(c)
value, err := cpe.New(c)
if err != nil {
log.Warnf("excluding invalid CPE %q: %v", c, err)
continue
+1
View File
@@ -15,6 +15,7 @@ func TestTableEncoder(t *testing.T) {
testutils.AssertEncoderAgainstGoldenSnapshot(t,
Format(),
testutils.DirectoryInput(t),
false,
*updateTableGoldenFiles,
)
}
+1
View File
@@ -19,6 +19,7 @@ func TestFormatWithOption(t *testing.T) {
f,
testutils.DirectoryInput(t),
*updateTmpl,
false,
)
}
+14 -14
View File
@@ -24,7 +24,7 @@
"cpe:2.3:a:alpine:alpine-baselayout:3.2.0-r16:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine:alpine_baselayout:3.2.0-r16:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/alpine-baselayout@3.2.0-r16?arch=x86_64",
"purl": "pkg:apk/alpine/alpine-baselayout@3.2.0-r16?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "alpine-baselayout",
@@ -504,7 +504,7 @@
"cpe:2.3:a:alpine:alpine-keys:2.3-r1:*:*:*:*:*:*:*",
"cpe:2.3:a:alpine:alpine_keys:2.3-r1:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/alpine-keys@2.3-r1?arch=x86_64",
"purl": "pkg:apk/alpine/alpine-keys@2.3-r1?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "alpine-keys",
@@ -790,7 +790,7 @@
"cpe:2.3:a:apk:apk-tools:2.12.7-r0:*:*:*:*:*:*:*",
"cpe:2.3:a:apk:apk_tools:2.12.7-r0:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/apk-tools@2.12.7-r0?arch=x86_64",
"purl": "pkg:apk/alpine/apk-tools@2.12.7-r0?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "apk-tools",
@@ -882,7 +882,7 @@
"cpes": [
"cpe:2.3:a:busybox:busybox:1.33.1-r3:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/busybox@1.33.1-r3?arch=x86_64",
"purl": "pkg:apk/alpine/busybox@1.33.1-r3?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "busybox",
@@ -1059,7 +1059,7 @@
"cpe:2.3:a:ca:ca-certificates-bundle:20191127-r5:*:*:*:*:*:*:*",
"cpe:2.3:a:ca:ca_certificates_bundle:20191127-r5:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/ca-certificates-bundle@20191127-r5?arch=x86_64",
"purl": "pkg:apk/alpine/ca-certificates-bundle@20191127-r5?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "ca-certificates-bundle",
@@ -1131,7 +1131,7 @@
"cpe:2.3:a:libc:libc-utils:0.7.2-r3:*:*:*:*:*:*:*",
"cpe:2.3:a:libc:libc_utils:0.7.2-r3:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/libc-utils@0.7.2-r3?arch=x86_64",
"purl": "pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "libc-utils",
@@ -1169,7 +1169,7 @@
"cpes": [
"cpe:2.3:a:libcrypto1.1:libcrypto1.1:1.1.1l-r0:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/libcrypto1.1@1.1.1l-r0?arch=x86_64",
"purl": "pkg:apk/alpine/libcrypto1.1@1.1.1l-r0?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "libcrypto1.1",
@@ -1347,7 +1347,7 @@
"cpes": [
"cpe:2.3:a:libretls:libretls:3.3.3p1-r2:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/libretls@3.3.3p1-r2?arch=x86_64",
"purl": "pkg:apk/alpine/libretls@3.3.3p1-r2?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "libretls",
@@ -1412,7 +1412,7 @@
"cpes": [
"cpe:2.3:a:libssl1.1:libssl1.1:1.1.1l-r0:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/libssl1.1@1.1.1l-r0?arch=x86_64",
"purl": "pkg:apk/alpine/libssl1.1@1.1.1l-r0?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "libssl1.1",
@@ -1480,7 +1480,7 @@
"cpes": [
"cpe:2.3:a:musl:musl:1.2.2-r3:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/musl@1.2.2-r3?arch=x86_64",
"purl": "pkg:apk/alpine/musl@1.2.2-r3?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "musl",
@@ -1549,7 +1549,7 @@
"cpe:2.3:a:musl:musl-utils:1.2.2-r3:*:*:*:*:*:*:*",
"cpe:2.3:a:musl:musl_utils:1.2.2-r3:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/musl-utils@1.2.2-r3?arch=x86_64",
"purl": "pkg:apk/alpine/musl-utils@1.2.2-r3?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "musl-utils",
@@ -1647,7 +1647,7 @@
"cpes": [
"cpe:2.3:a:scanelf:scanelf:1.3.2-r0:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/scanelf@1.3.2-r0?arch=x86_64",
"purl": "pkg:apk/alpine/scanelf@1.3.2-r0?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "scanelf",
@@ -1707,7 +1707,7 @@
"cpe:2.3:a:ssl:ssl-client:1.33.1-r3:*:*:*:*:*:*:*",
"cpe:2.3:a:ssl:ssl_client:1.33.1-r3:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/ssl_client@1.33.1-r3?arch=x86_64",
"purl": "pkg:apk/alpine/ssl_client@1.33.1-r3?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "ssl_client",
@@ -1762,7 +1762,7 @@
"cpes": [
"cpe:2.3:a:zlib:zlib:1.2.11-r3:*:*:*:*:*:*:*"
],
"purl": "pkg:alpine/zlib@1.2.11-r3?arch=x86_64",
"purl": "pkg:apk/alpine/zlib@1.2.11-r3?arch=x86_64",
"metadataType": "ApkMetadata",
"metadata": {
"package": "zlib",
+2
View File
@@ -14,6 +14,7 @@ func TestTextDirectoryEncoder(t *testing.T) {
Format(),
testutils.DirectoryInput(t),
*updateTextEncoderGoldenFiles,
false,
)
}
@@ -24,5 +25,6 @@ func TestTextImageEncoder(t *testing.T) {
testutils.ImageInput(t, testImage, testutils.FromSnapshot()),
testImage,
*updateTextEncoderGoldenFiles,
false,
)
}
+1 -1
View File
@@ -69,7 +69,7 @@ func CatalogPackages(src *source.Source, cfg cataloger.Config) (*pkg.Catalog, []
}
}
catalog, relationships, err := cataloger.Catalog(resolver, release, catalogers...)
catalog, relationships, err := cataloger.Catalog(resolver, release, cfg.Parallelism, catalogers...)
if err != nil {
return nil, nil, nil, err
}
+83 -19
View File
@@ -26,6 +26,15 @@ loopNewIDs:
}
}
func (s *orderedIDSet) delete(id artifact.ID) {
for i, existingID := range s.slice {
if existingID == id {
s.slice = append(s.slice[:i], s.slice[i+1:]...)
return
}
}
}
// Catalog represents a collection of Packages.
type Catalog struct {
byID map[artifact.ID]Package
@@ -92,30 +101,32 @@ func (c *Catalog) Packages(ids []artifact.ID) (result []Package) {
return result
}
// Add a package to the Catalog.
func (c *Catalog) Add(p Package) {
// Add n packages to the catalog.
func (c *Catalog) Add(pkgs ...Package) {
c.lock.Lock()
defer c.lock.Unlock()
id := p.ID()
if id == "" {
log.Warnf("found package with empty ID while adding to the catalog: %+v", p)
p.SetID()
id = p.ID()
}
if existing, exists := c.byID[id]; exists {
// there is already a package with this fingerprint merge the existing record with the new one
if err := existing.merge(p); err != nil {
log.Warnf("failed to merge packages: %+v", err)
} else {
c.byID[id] = existing
c.addPathsToIndex(p)
for _, p := range pkgs {
id := p.ID()
if id == "" {
log.Warnf("found package with empty ID while adding to the catalog: %+v", p)
p.SetID()
id = p.ID()
}
return
}
c.addToIndex(p)
if existing, exists := c.byID[id]; exists {
// there is already a package with this fingerprint merge the existing record with the new one
if err := existing.merge(p); err != nil {
log.Warnf("failed to merge packages: %+v", err)
} else {
c.byID[id] = existing
c.addPathsToIndex(p)
}
return
}
c.addToIndex(p)
}
}
func (c *Catalog) addToIndex(p Package) {
@@ -157,6 +168,59 @@ func (c *Catalog) addPathToIndex(id artifact.ID, path string) {
c.idsByPath[path] = pathIndex
}
func (c *Catalog) Delete(ids ...artifact.ID) {
c.lock.Lock()
defer c.lock.Unlock()
for _, id := range ids {
p, exists := c.byID[id]
if !exists {
return
}
delete(c.byID, id)
c.deleteNameFromIndex(p)
c.deleteTypeFromIndex(p)
c.deletePathsFromIndex(p)
}
}
func (c *Catalog) deleteNameFromIndex(p Package) {
nameIndex := c.idsByName[p.Name]
nameIndex.delete(p.id)
c.idsByName[p.Name] = nameIndex
}
func (c *Catalog) deleteTypeFromIndex(p Package) {
typeIndex := c.idsByType[p.Type]
typeIndex.delete(p.id)
c.idsByType[p.Type] = typeIndex
}
func (c *Catalog) deletePathsFromIndex(p Package) {
observedPaths := internal.NewStringSet()
for _, l := range p.Locations.ToSlice() {
if l.RealPath != "" && !observedPaths.Contains(l.RealPath) {
c.deletePathFromIndex(p.id, l.RealPath)
observedPaths.Add(l.RealPath)
}
if l.VirtualPath != "" && l.RealPath != l.VirtualPath && !observedPaths.Contains(l.VirtualPath) {
c.deletePathFromIndex(p.id, l.VirtualPath)
observedPaths.Add(l.VirtualPath)
}
}
}
func (c *Catalog) deletePathFromIndex(id artifact.ID, path string) {
pathIndex := c.idsByPath[path]
pathIndex.delete(id)
if len(pathIndex.slice) == 0 {
delete(c.idsByPath, path)
} else {
c.idsByPath[path] = pathIndex
}
}
// Enumerate all packages for the given type(s), enumerating all packages if no type is specified.
func (c *Catalog) Enumerate(types ...Type) <-chan Package {
channel := make(chan Package)
+145 -2
View File
@@ -8,6 +8,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/source"
)
@@ -16,6 +17,148 @@ type expectedIndexes struct {
byPath map[string]*strset.Set
}
func TestCatalogDeleteRemovesPackages(t *testing.T) {
tests := []struct {
name string
pkgs []Package
deleteIDs []artifact.ID
expectedIndexes expectedIndexes
}{
{
name: "delete one package",
pkgs: []Package{
{
id: "pkg:deb/debian/1",
Name: "debian",
Version: "1",
Type: DebPkg,
Locations: source.NewLocationSet(
source.NewVirtualLocation("/c/path", "/another/path1"),
),
},
{
id: "pkg:deb/debian/2",
Name: "debian",
Version: "2",
Type: DebPkg,
Locations: source.NewLocationSet(
source.NewVirtualLocation("/d/path", "/another/path2"),
),
},
},
deleteIDs: []artifact.ID{
artifact.ID("pkg:deb/debian/1"),
},
expectedIndexes: expectedIndexes{
byType: map[Type]*strset.Set{
DebPkg: strset.New("pkg:deb/debian/2"),
},
byPath: map[string]*strset.Set{
"/d/path": strset.New("pkg:deb/debian/2"),
"/another/path2": strset.New("pkg:deb/debian/2"),
},
},
},
{
name: "delete multiple packages",
pkgs: []Package{
{
id: "pkg:deb/debian/1",
Name: "debian",
Version: "1",
Type: DebPkg,
Locations: source.NewLocationSet(
source.NewVirtualLocation("/c/path", "/another/path1"),
),
},
{
id: "pkg:deb/debian/2",
Name: "debian",
Version: "2",
Type: DebPkg,
Locations: source.NewLocationSet(
source.NewVirtualLocation("/d/path", "/another/path2"),
),
},
{
id: "pkg:deb/debian/3",
Name: "debian",
Version: "3",
Type: DebPkg,
Locations: source.NewLocationSet(
source.NewVirtualLocation("/e/path", "/another/path3"),
),
},
},
deleteIDs: []artifact.ID{
artifact.ID("pkg:deb/debian/1"),
artifact.ID("pkg:deb/debian/3"),
},
expectedIndexes: expectedIndexes{
byType: map[Type]*strset.Set{
DebPkg: strset.New("pkg:deb/debian/2"),
},
byPath: map[string]*strset.Set{
"/d/path": strset.New("pkg:deb/debian/2"),
"/another/path2": strset.New("pkg:deb/debian/2"),
},
},
},
{
name: "delete non-existent package",
pkgs: []Package{
{
id: artifact.ID("pkg:deb/debian/1"),
Name: "debian",
Version: "1",
Type: DebPkg,
Locations: source.NewLocationSet(
source.NewVirtualLocation("/c/path", "/another/path1"),
),
},
{
id: artifact.ID("pkg:deb/debian/2"),
Name: "debian",
Version: "2",
Type: DebPkg,
Locations: source.NewLocationSet(
source.NewVirtualLocation("/d/path", "/another/path2"),
),
},
},
deleteIDs: []artifact.ID{
artifact.ID("pkg:deb/debian/3"),
},
expectedIndexes: expectedIndexes{
byType: map[Type]*strset.Set{
DebPkg: strset.New("pkg:deb/debian/1", "pkg:deb/debian/2"),
},
byPath: map[string]*strset.Set{
"/c/path": strset.New("pkg:deb/debian/1"),
"/another/path1": strset.New("pkg:deb/debian/1"),
"/d/path": strset.New("pkg:deb/debian/2"),
"/another/path2": strset.New("pkg:deb/debian/2"),
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := NewCatalog()
for _, p := range test.pkgs {
c.Add(p)
}
for _, id := range test.deleteIDs {
c.Delete(id)
}
assertIndexes(t, c, test.expectedIndexes)
})
}
}
func TestCatalogAddPopulatesIndex(t *testing.T) {
var pkgs = []Package{
@@ -182,7 +325,7 @@ func TestCatalog_MergeRecords(t *testing.T) {
name: "multiple Locations with shared path",
pkgs: []Package{
{
CPEs: []CPE{MustCPE("cpe:2.3:a:package:1:1:*:*:*:*:*:*:*")},
CPEs: []cpe.CPE{cpe.Must("cpe:2.3:a:package:1:1:*:*:*:*:*:*:*")},
Locations: source.NewLocationSet(
source.Location{
Coordinates: source.Coordinates{
@@ -195,7 +338,7 @@ func TestCatalog_MergeRecords(t *testing.T) {
Type: RpmPkg,
},
{
CPEs: []CPE{MustCPE("cpe:2.3:b:package:1:1:*:*:*:*:*:*:*")},
CPEs: []cpe.CPE{cpe.Must("cpe:2.3:b:package:1:1:*:*:*:*:*:*:*")},
Locations: source.NewLocationSet(
source.Location{
Coordinates: source.Coordinates{
+1 -3
View File
@@ -42,10 +42,8 @@ func packageURL(m pkg.ApkMetadata, distro *linux.Release) string {
}
return packageurl.NewPackageURL(
// note: this is currently a candidate and not technically within spec
// see https://github.com/package-url/purl-spec#other-candidate-types-to-define
packageurl.TypeAlpine,
"alpine",
"",
m.Package,
m.Version,
pkg.PURLQualifiers(
+5 -5
View File
@@ -43,7 +43,7 @@ func Test_PackageURL(t *testing.T) {
ID: "alpine",
VersionID: "3.4.6",
},
expected: "pkg:alpine/p@v?arch=a&distro=alpine-3.4.6",
expected: "pkg:apk/alpine/p@v?arch=a&distro=alpine-3.4.6",
},
{
name: "missing architecture",
@@ -55,7 +55,7 @@ func Test_PackageURL(t *testing.T) {
ID: "alpine",
VersionID: "3.4.6",
},
expected: "pkg:alpine/p@v?distro=alpine-3.4.6",
expected: "pkg:apk/alpine/p@v?distro=alpine-3.4.6",
},
// verify #351
{
@@ -68,7 +68,7 @@ func Test_PackageURL(t *testing.T) {
ID: "alpine",
VersionID: "3.4.6",
},
expected: "pkg:alpine/g++@v84?arch=am86&distro=alpine-3.4.6",
expected: "pkg:apk/alpine/g++@v84?arch=am86&distro=alpine-3.4.6",
},
{
metadata: pkg.ApkMetadata{
@@ -80,7 +80,7 @@ func Test_PackageURL(t *testing.T) {
ID: "alpine",
VersionID: "3.15.0",
},
expected: "pkg:alpine/g%20plus%20plus@v84?arch=am86&distro=alpine-3.15.0",
expected: "pkg:apk/alpine/g%20plus%20plus@v84?arch=am86&distro=alpine-3.15.0",
},
{
name: "add source information as qualifier",
@@ -94,7 +94,7 @@ func Test_PackageURL(t *testing.T) {
ID: "alpine",
VersionID: "3.4.6",
},
expected: "pkg:alpine/p@v?arch=a&upstream=origin&distro=alpine-3.4.6",
expected: "pkg:apk/alpine/p@v?arch=a&upstream=origin&distro=alpine-3.4.6",
},
}
+231 -125
View File
@@ -3,13 +3,10 @@ package apkdb
import (
"bufio"
"fmt"
"io"
"path"
"strconv"
"strings"
"github.com/mitchellh/mapstructure"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
@@ -22,28 +19,73 @@ import (
// integrity check
var _ generic.Parser = 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 .
// parseApkDB parses packages from a given APK installed DB file. For more
// information on specific fields, see https://wiki.alpinelinux.org/wiki/Apk_spec.
//
//nolint:funlen
func parseApkDB(_ source.FileResolver, env *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
// larger capacity for the scanner.
const maxScannerCapacity = 1024 * 1024
// a new larger buffer for the scanner
bufScan := make([]byte, maxScannerCapacity)
pkgs := 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' {
return i + 1, data[:i-1], nil
var apks []pkg.ApkMetadata
var currentEntry pkg.ApkMetadata
entryParsingInProgress := false
fileParsingCtx := newApkFileParsingContext()
// creating a dedicated append-like function here instead of using `append(...)`
// below since there is nontrivial logic to be performed for each finalized apk
// entry.
appendApk := func(p pkg.ApkMetadata) {
if files := fileParsingCtx.files; len(files) >= 1 {
// attached accumulated files to current package
p.Files = files
// reset file parsing for next use
fileParsingCtx = newApkFileParsingContext()
}
nilFieldsToEmptySlice(&p)
apks = append(apks, p)
}
for scanner.Scan() {
line := scanner.Text()
if line == "" {
// i.e. apk entry separator
if entryParsingInProgress {
// current entry is complete
appendApk(currentEntry)
}
entryParsingInProgress = false
// zero-out currentEntry for use by any future entry
currentEntry = pkg.ApkMetadata{}
continue
}
if !atEOF {
return 0, nil, nil
field := parseApkField(line)
if field == nil {
log.Warnf("unable to parse field data from line %q", line)
continue
}
// deliver the last token (which could be an empty string)
return 0, data, bufio.ErrFinalToken
entryParsingInProgress = true
field.apply(&currentEntry, fileParsingCtx)
}
if entryParsingInProgress {
// There was no final empty line, so currentEntry hasn't been added to the
// collection yet; but we've now reached the end of scanning, so let's be sure to
// add currentEntry to the collection.
appendApk(currentEntry)
}
if err := scanner.Err(); err != nil {
return nil, nil, fmt.Errorf("failed to parse APK installed DB file: %w", err)
}
var r *linux.Release
@@ -51,128 +93,192 @@ func parseApkDB(_ source.FileResolver, env *generic.Environment, reader source.L
r = env.LinuxRelease
}
scanner.Split(onDoubleLF)
for scanner.Scan() {
metadata, err := parseApkDBEntry(strings.NewReader(scanner.Text()))
if err != nil {
return nil, nil, err
}
if metadata != nil {
pkgs = append(pkgs, newPackage(*metadata, r, reader.Location))
}
}
if err := scanner.Err(); err != nil {
return nil, nil, fmt.Errorf("failed to parse APK DB file: %w", err)
pkgs := make([]pkg.Package, 0, len(apks))
for _, apk := range apks {
pkgs = append(pkgs, newPackage(apk, r, reader.Location))
}
return pkgs, discoverPackageDependencies(pkgs), nil
}
// parseApkDBEntry reads and parses a single pkg.ApkMetadata element from the stream, returning nil if their are no more entries.
//
func parseApkField(line string) *apkField {
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
return nil
}
f := apkField{
name: parts[0],
value: parts[1],
}
return &f
}
type apkField struct {
name string
value string
}
//nolint:funlen
func parseApkDBEntry(reader io.Reader) (*pkg.ApkMetadata, error) {
var entry pkg.ApkMetadata
pkgFields := make(map[string]interface{})
func (f apkField) apply(p *pkg.ApkMetadata, ctx *apkFileParsingContext) {
switch f.name {
// APKINDEX field parsing
// We want sane defaults for collections, i.e. an empty array instead of null.
pkgFields["D"] = []string{}
pkgFields["p"] = []string{}
files := make([]pkg.ApkFileRecord, 0)
var fileRecord *pkg.ApkFileRecord
lastFile := "/"
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()
fields := strings.SplitN(line, ":", 2)
if len(fields) != 2 {
continue
case "P":
p.Package = f.value
case "o":
p.OriginPackage = f.value
case "m":
p.Maintainer = f.value
case "V":
p.Version = f.value
case "L":
p.License = f.value
case "A":
p.Architecture = f.value
case "U":
p.URL = f.value
case "T":
p.Description = f.value
case "S":
i, err := strconv.Atoi(f.value)
if err != nil {
log.Warnf("unable to parse value %q for field %q: %w", f.value, f.name, err)
return
}
key := fields[0]
value := strings.TrimSpace(fields[1])
switch key {
case "D", "p":
entries := strings.Split(value, " ")
pkgFields[key] = entries
case "F":
currentFile := "/" + value
newFileRecord := pkg.ApkFileRecord{
Path: currentFile,
}
files = append(files, newFileRecord)
fileRecord = &files[len(files)-1]
// future aux references are relative to previous "F" records
lastFile = currentFile
continue
case "R":
newFileRecord := pkg.ApkFileRecord{
Path: path.Join(lastFile, value),
}
files = append(files, newFileRecord)
fileRecord = &files[len(files)-1]
case "a", "M":
ownershipFields := strings.Split(value, ":")
if len(ownershipFields) < 3 {
log.Warnf("unexpected APK ownership field: %q", value)
continue
}
if fileRecord == nil {
log.Warnf("ownership field with no parent record: %q", value)
continue
}
fileRecord.OwnerUID = ownershipFields[0]
fileRecord.OwnerGID = 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.Warnf("checksum field with no parent record: %q", value)
continue
}
fileRecord.Digest = processChecksum(value)
case "I", "S":
// coerce to integer
iVal, err := strconv.Atoi(value)
if err != nil {
return nil, fmt.Errorf("failed to parse APK int: '%+v'", value)
}
pkgFields[key] = iVal
default:
pkgFields[key] = value
p.Size = i
case "I":
i, err := strconv.Atoi(f.value)
if err != nil {
log.Warnf("unable to parse value %q for field %q: %w", f.value, f.name, err)
return
}
p.InstalledSize = i
case "D":
deps := parseListValue(f.value)
p.Dependencies = deps
case "p":
provides := parseListValue(f.value)
p.Provides = provides
case "C":
p.Checksum = f.value
case "c":
p.GitCommit = f.value
// File/directory field parsing:
case "F":
directory := path.Join("/", f.value)
ctx.files = append(ctx.files, pkg.ApkFileRecord{Path: directory})
ctx.indexOfLatestDirectory = len(ctx.files) - 1
case "M":
i := ctx.indexOfLatestDirectory
latest := ctx.files[i]
var ok bool
latest.OwnerUID, latest.OwnerGID, latest.Permissions, ok = processFileInfo(f.value)
if !ok {
log.Warnf("unexpected value for APK ACL field %q: %q", f.name, f.value)
return
}
// save updated directory
ctx.files[i] = latest
case "R":
var regularFile string
dirIndex := ctx.indexOfLatestDirectory
if dirIndex < 0 {
regularFile = path.Join("/", f.value)
} else {
latestDirPath := ctx.files[dirIndex].Path
regularFile = path.Join(latestDirPath, f.value)
}
ctx.files = append(ctx.files, pkg.ApkFileRecord{Path: regularFile})
ctx.indexOfLatestRegularFile = len(ctx.files) - 1
case "a":
i := ctx.indexOfLatestRegularFile
latest := ctx.files[i]
var ok bool
latest.OwnerUID, latest.OwnerGID, latest.Permissions, ok = processFileInfo(f.value)
if !ok {
log.Warnf("unexpected value for APK ACL field %q: %q", f.name, f.value)
return
}
// save updated file
ctx.files[i] = latest
case "Z":
i := ctx.indexOfLatestRegularFile
latest := ctx.files[i]
latest.Digest = processChecksum(f.value)
// save updated file
ctx.files[i] = latest
}
}
func processFileInfo(v string) (uid, gid, perms string, ok bool) {
ok = false
fileInfo := strings.Split(v, ":")
if len(fileInfo) < 3 {
return
}
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
// By default, mapstructure compares field names in a *case-insensitive* manner.
// That would be the wrong approach here, since these apk files use case
// *sensitive* field names (e.g. 'P' vs. 'p').
MatchName: func(mapKey, fieldName string) bool {
return mapKey == fieldName
},
Result: &entry,
})
if err != nil {
return nil, err
uid = fileInfo[0]
gid = fileInfo[1]
perms = fileInfo[2]
// note: there are more optional fields available that we are not capturing,
// e.g.: "0:0:755:Q1JaDEHQHBbizhEzoWK1YxuraNU/4="
ok = true
return
}
// apkFileParsingContext helps keep track of what file data has been captured so far for the APK currently being parsed.
type apkFileParsingContext struct {
files []pkg.ApkFileRecord
indexOfLatestDirectory int
indexOfLatestRegularFile int
}
func newApkFileParsingContext() *apkFileParsingContext {
return &apkFileParsingContext{
indexOfLatestDirectory: -1, // no directories yet
indexOfLatestRegularFile: -1, // no regular files yet
}
}
// parseListValue parses a space-separated list from an apk entry field value.
func parseListValue(value string) []string {
items := strings.Split(value, " ")
if len(items) >= 1 {
return items
}
if err := decoder.Decode(pkgFields); err != nil {
return nil, fmt.Errorf("unable to parse APK metadata: %w", err)
}
if entry.Package == "" {
return nil, nil
return nil
}
func nilFieldsToEmptySlice(p *pkg.ApkMetadata) {
if p.Dependencies == nil {
p.Dependencies = []string{}
}
entry.Files = files
if p.Provides == nil {
p.Provides = []string{}
}
return &entry, nil
if p.Files == nil {
p.Files = []pkg.ApkFileRecord{}
}
}
func processChecksum(value string) *file.Digest {

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