├── .go-version ├── hack └── boilerplate │ ├── boilerplate.yaml.txt │ ├── boilerplate.yml.txt │ ├── boilerplate.go.txt │ └── boilerplate.sh.txt ├── main.go ├── pkg ├── ctl │ ├── testdata │ │ ├── document2.vex.json │ │ ├── document1.vex.json │ │ └── test.vex.json │ ├── ctl_test.go │ ├── ctl.go │ └── implementation.go ├── attestation │ ├── attestation_test.go │ └── attestation.go ├── vex │ ├── testdata │ │ ├── vex.yaml │ │ └── csaf.json │ ├── status.go │ ├── justification.go │ ├── vex_test.go │ ├── statement.go │ └── vex.go ├── sarif │ └── sarif.go └── csaf │ ├── csaf_test.go │ ├── testdata │ └── csaf.json │ └── csaf.go ├── .github ├── dependabot.yml └── workflows │ ├── verify.yaml │ ├── ci-build-test.yaml │ ├── boilerplate.yaml │ └── release.yaml ├── Makefile ├── internal └── cmd │ ├── attest.go │ ├── main.go │ ├── merge.go │ ├── filter.go │ └── create.go ├── .goreleaser.yaml ├── .gitignore ├── .golangci.yml ├── README.md ├── LICENSE └── go.mod /.go-version: -------------------------------------------------------------------------------- 1 | 1.19.2 2 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.yaml.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Chainguard, Inc. 2 | # SPDX-License-Identifier: Apache-2.0 3 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.yml.txt: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Chainguard, Inc. 2 | # SPDX-License-Identifier: Apache-2.0 3 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | -------------------------------------------------------------------------------- /hack/boilerplate/boilerplate.sh.txt: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2022 Chainguard, Inc. 4 | # SPDX-License-Identifier: Apache-2.0 5 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package main 7 | 8 | import ( 9 | "chainguard.dev/vex/internal/cmd" 10 | ) 11 | 12 | func main() { 13 | cmd.Execute() 14 | } 15 | -------------------------------------------------------------------------------- /pkg/ctl/testdata/document2.vex.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "my-vexdoc", 3 | "format": "text/vex+json", 4 | "author": "John Doe", 5 | "role": "vex issuer", 6 | "statements": [ 7 | { 8 | "timestamp": "2022-12-22T20:56:05-05:00", 9 | "products": ["pkg:apk/wolfi/bash@1.0.0"], 10 | "vulnerability": "CVE-1234-5678", 11 | "status": "affected" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /pkg/ctl/testdata/document1.vex.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "my-vexdoc", 3 | "format": "text/vex+json", 4 | "author": "John Doe", 5 | "role": "vex issuer", 6 | "statements": [ 7 | { 8 | "timestamp": "2022-12-22T16:36:43-05:00", 9 | "products": ["pkg:apk/wolfi/bash@1.0.0"], 10 | "vulnerability": "CVE-1234-5678", 11 | "status": "under_investigation", 12 | "status_notes": "" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /pkg/ctl/testdata/test.vex.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "", 3 | "author": "Chainguard", 4 | "role": "author", 5 | "timestamp": "2022-08-29T17:48:53.697543267-05:00", 6 | "statements": [ 7 | { 8 | "vulnerability": "CVE-2009-4487", 9 | "status": "not_affected", 10 | "justification": "vulnerable_code_not_in_execute_path", 11 | "action_statement": "Affected library function not called" 12 | }, 13 | { 14 | "vulnerability": "CVE-2021-44228", 15 | "status": "affected", 16 | "action_statement": "Customers are advised to upgrade" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Chainguard, Inc. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "github-actions" 7 | # github-actions / path starts .github/workflows 8 | directory: "/" 9 | schedule: 10 | # Run every weekday 11 | interval: "daily" 12 | open-pull-requests-limit: 10 13 | labels: 14 | - "kind/other" 15 | - "release-note-none" 16 | 17 | - package-ecosystem: gomod 18 | directory: "/" 19 | schedule: 20 | interval: "daily" 21 | open-pull-requests-limit: 10 22 | labels: 23 | - "kind/other" 24 | - "release-note-none" 25 | -------------------------------------------------------------------------------- /pkg/attestation/attestation_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package attestation 7 | 8 | import ( 9 | "bytes" 10 | "encoding/json" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | 15 | "chainguard.dev/vex/pkg/vex" 16 | ) 17 | 18 | func TestSerialize(t *testing.T) { 19 | att := New() 20 | pred := vex.New() 21 | pred.Author = "Chainguard" 22 | att.Predicate = pred 23 | 24 | var b bytes.Buffer 25 | err := att.ToJSON(&b) 26 | require.NoError(t, err) 27 | 28 | att2 := New() 29 | err = json.Unmarshal(b.Bytes(), &att2) 30 | require.NoError(t, err) 31 | require.Equal(t, att2.Predicate.Author, "Chainguard") 32 | } 33 | -------------------------------------------------------------------------------- /pkg/vex/testdata/vex.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | format: vex_attestation 3 | id: "" 4 | author: "Chainguard" 5 | role: "author" 6 | timestamp: "2022-08-29T17:48:53.697543267-05:00" 7 | statements: 8 | - vulnerability: "CVE-2022-31030" 9 | status: "not_affected" 10 | justification: "vulnerable_code_not_in_execute_path" 11 | action_statement: "Affected library function not called" 12 | references: 13 | - type: FEDORA 14 | ref: "FEDORA-2022-1da581ac6d" 15 | - type: "DEBIAN" 16 | ref: "DSA-5162" 17 | - vulnerability: "CVE-2021-44228" # Log4j 18 | status: "affected" 19 | action_statement: "Customers are advised to upgrade" 20 | references: 21 | - type: FEDORA 22 | ref: "FEDORA-2021-66d6c484f3" 23 | - type: "CERT-VN" 24 | ref: "VU#930724" 25 | - type: "CISCO" 26 | ref: "cisco-sa-apache-log4j-qRuKNEbd" 27 | -------------------------------------------------------------------------------- /.github/workflows/verify.yaml: -------------------------------------------------------------------------------- 1 | name: verify 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | golangci: 13 | name: golangci-lint 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 17 | - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3 18 | with: 19 | go-version: 1.19 20 | check-latest: true 21 | cache: true 22 | - name: golangci-lint 23 | uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5 # v3 24 | with: 25 | # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. 26 | version: v1.49 27 | args: --timeout=5m 28 | -------------------------------------------------------------------------------- /pkg/sarif/sarif.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package sarif 7 | 8 | import ( 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "os" 13 | 14 | gosarif "github.com/owenrumney/go-sarif/sarif" 15 | ) 16 | 17 | type Report struct { 18 | gosarif.Report 19 | } 20 | 21 | func Open(path string) (*Report, error) { 22 | data, err := os.ReadFile(path) 23 | if err != nil { 24 | return nil, fmt.Errorf("opening yaml file: %w", err) 25 | } 26 | report := New() 27 | if err := json.Unmarshal(data, report); err != nil { 28 | return nil, fmt.Errorf("unmarshalling vex data: %w", err) 29 | } 30 | return report, nil 31 | } 32 | 33 | func New() *Report { 34 | return &Report{ 35 | Report: gosarif.Report{}, 36 | } 37 | } 38 | 39 | func (report *Report) ToJSON(w io.Writer) error { 40 | enc := json.NewEncoder(w) 41 | enc.SetIndent("", " ") 42 | enc.SetEscapeHTML(false) 43 | 44 | if err := enc.Encode(report); err != nil { 45 | return fmt.Errorf("encoding sarif report: %w", err) 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/ci-build-test.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Chainguard, Inc. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | name: ci-build-test 5 | 6 | on: 7 | push: 8 | branches: 9 | - "main" 10 | pull_request: 11 | 12 | jobs: 13 | build: 14 | name: build 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v2.4.0 19 | 20 | - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v2.2.0 21 | with: 22 | go-version: '1.19' 23 | check-latest: true 24 | cache: true 25 | 26 | - name: build 27 | run: make vex 28 | 29 | test: 30 | name: test 31 | runs-on: ubuntu-latest 32 | 33 | steps: 34 | - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v2.4.0 35 | 36 | - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v2.2.0 37 | with: 38 | go-version: '1.19' 39 | check-latest: true 40 | cache: true 41 | 42 | - name: test 43 | run: make test 44 | -------------------------------------------------------------------------------- /.github/workflows/boilerplate.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Chainguard, Inc. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | name: Boilerplate 5 | 6 | on: 7 | pull_request: 8 | branches: [ 'main', 'release-*' ] 9 | 10 | jobs: 11 | 12 | check: 13 | name: Boilerplate Check 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false # Keep running if one leg fails. 17 | matrix: 18 | extension: 19 | - go 20 | - sh 21 | - yaml 22 | 23 | # Map between extension and human-readable name. 24 | include: 25 | - extension: go 26 | language: Go 27 | - extension: sh 28 | language: Bash 29 | - extension: yaml 30 | language: YAML 31 | - extension: yml 32 | language: YAML 33 | 34 | steps: 35 | - name: Check out code 36 | uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c 37 | 38 | - uses: chainguard-dev/actions/boilerplate@main 39 | with: 40 | extension: ${{ matrix.extension }} 41 | language: ${{ matrix.language }} 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | 12 | permissions: 13 | id-token: write 14 | contents: write 15 | 16 | env: 17 | GO111MODULE: on 18 | COSIGN_EXPERIMENTAL: "true" 19 | 20 | steps: 21 | - name: Check out code onto GOPATH 22 | uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.0.2 23 | 24 | - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.3.0 25 | with: 26 | go-version: 1.19 27 | check-latest: true 28 | 29 | - name: Install cosign 30 | uses: sigstore/cosign-installer@9becc617647dfa20ae7b1151972e9b3a2c338a2b # v2 31 | 32 | - name: Install GoReleaser 33 | uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b # v3.1.0 34 | with: 35 | install-only: true 36 | 37 | - name: Get TAG 38 | id: get_tag 39 | run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 40 | 41 | - name: Run goreleaser 42 | run: make release 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | -------------------------------------------------------------------------------- /pkg/csaf/csaf_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package csaf 7 | 8 | import ( 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestOpen(t *testing.T) { 15 | doc, err := Open("testdata/csaf.json") 16 | require.NoError(t, err) 17 | require.NotNil(t, doc) 18 | require.Equal(t, "Example VEX Document", doc.Document.Title) 19 | require.Equal(t, "CSAFPID-0001", doc.FirstProductName()) 20 | 21 | // Vulnerabilities 22 | require.Len(t, doc.Vulnerabilities, 1) 23 | require.Equal(t, doc.Vulnerabilities[0].CVE, "CVE-2009-4487") 24 | require.Len(t, doc.Vulnerabilities[0].ProductStatus, 1) 25 | require.Len(t, doc.Vulnerabilities[0].ProductStatus["known_not_affected"], 1) 26 | require.Equal(t, doc.Vulnerabilities[0].ProductStatus["known_not_affected"][0], "CSAFPID-0001") 27 | } 28 | 29 | func TestFindFirstProduct(t *testing.T) { 30 | doc, err := Open("testdata/csaf.json") 31 | require.NoError(t, err) 32 | require.NotNil(t, doc) 33 | 34 | prod := doc.ProductTree.FindFirstProduct() 35 | require.Equal(t, prod, "CSAFPID-0001") 36 | } 37 | 38 | func TestFindByHelper(t *testing.T) { 39 | doc, err := Open("testdata/csaf.json") 40 | require.NoError(t, err) 41 | require.NotNil(t, doc) 42 | 43 | prod := doc.ProductTree.FindProductIdentifier("purl", "pkg:maven/@1.3.4") 44 | require.NotNil(t, prod) 45 | require.Equal(t, prod.ID, "CSAFPID-0001") 46 | } 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Chainguard, Inc. 2 | # SPDX-License-Identifier: Apache-2.0 3 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 4 | ifeq (,$(shell go env GOBIN)) 5 | GOBIN=$(shell go env GOPATH)/bin 6 | else 7 | GOBIN=$(shell go env GOBIN) 8 | endif 9 | 10 | # Set version variables for LDFLAGS 11 | GIT_VERSION ?= $(shell git describe --tags --always --dirty) 12 | GIT_HASH ?= $(shell git rev-parse HEAD) 13 | DATE_FMT = +%Y-%m-%dT%H:%M:%SZ 14 | SOURCE_DATE_EPOCH ?= $(shell git log -1 --pretty=%ct) 15 | ifdef SOURCE_DATE_EPOCH 16 | BUILD_DATE ?= $(shell date -u -d "@$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u -r "$(SOURCE_DATE_EPOCH)" "$(DATE_FMT)" 2>/dev/null || date -u "$(DATE_FMT)") 17 | else 18 | BUILD_DATE ?= $(shell date "$(DATE_FMT)") 19 | endif 20 | GIT_TREESTATE = "clean" 21 | DIFF = $(shell git diff --quiet >/dev/null 2>&1; if [ $$? -eq 1 ]; then echo "1"; fi) 22 | ifeq ($(DIFF), 1) 23 | GIT_TREESTATE = "dirty" 24 | endif 25 | 26 | LDFLAGS=-buildid= -X sigs.k8s.io/release-utils/version.gitVersion=$(GIT_VERSION) \ 27 | -X sigs.k8s.io/release-utils/version.gitCommit=$(GIT_HASH) \ 28 | -X sigs.k8s.io/release-utils/version.gitTreeState=$(GIT_TREESTATE) \ 29 | -X sigs.k8s.io/release-utils/version.buildDate=$(BUILD_DATE) 30 | ## Build 31 | .PHONY: vex 32 | vex: # build the binaries 33 | go build -trimpath -ldflags "$(LDFLAGS)" -o vexctl ./main.go 34 | 35 | ## Tests 36 | .PHONY: test 37 | test: 38 | go test -v ./... 39 | 40 | ## Release 41 | 42 | .PHONY: release 43 | release: 44 | LDFLAGS="$(LDFLAGS)" goreleaser release --rm-dist --timeout 120m 45 | 46 | .PHONY: snapshot 47 | snapshot: 48 | LDFLAGS="$(LDFLAGS)" goreleaser release --rm-dist --snapshot --skip-sign --skip-publish --timeout 120m 49 | -------------------------------------------------------------------------------- /pkg/vex/status.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package vex 7 | 8 | // Status describes the exploitability status of a component with respect to a 9 | // vulnerability. 10 | type Status string 11 | 12 | const ( 13 | // StatusNotAffected means no remediation or mitigation is required. 14 | StatusNotAffected Status = "not_affected" 15 | 16 | // StatusAffected means actions are recommended to remediate or mitigate. 17 | StatusAffected Status = "affected" 18 | 19 | // StatusFixed means the listed products or components have been remediated (by including fixes). 20 | StatusFixed Status = "fixed" 21 | 22 | // StatusUnderInvestigation means the author of the VEX statement is investigating. 23 | StatusUnderInvestigation Status = "under_investigation" 24 | ) 25 | 26 | // Statuses returns a list of the valid Status values. 27 | func Statuses() []string { 28 | return []string{ 29 | string(StatusNotAffected), 30 | string(StatusAffected), 31 | string(StatusFixed), 32 | string(StatusUnderInvestigation), 33 | } 34 | } 35 | 36 | // Valid returns a bool indicating whether the Status value is equal to one of the enumerated allowed values for Status. 37 | func (s Status) Valid() bool { 38 | switch s { 39 | case StatusNotAffected, 40 | StatusAffected, 41 | StatusFixed, 42 | StatusUnderInvestigation: 43 | 44 | return true 45 | 46 | default: 47 | 48 | return false 49 | } 50 | } 51 | 52 | // StatusFromCSAF returns a vex status from the CSAF status 53 | func StatusFromCSAF(csafStatus string) Status { 54 | switch csafStatus { 55 | case "known_not_affected": 56 | return StatusNotAffected 57 | case "fixed": 58 | return StatusFixed 59 | case "under_investigation": 60 | return StatusUnderInvestigation 61 | case "known_affected": 62 | return StatusAffected 63 | default: 64 | return "" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /internal/cmd/attest.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package cmd 7 | 8 | import ( 9 | "context" 10 | "errors" 11 | "fmt" 12 | "os" 13 | 14 | "chainguard.dev/vex/pkg/ctl" 15 | "github.com/spf13/cobra" 16 | ) 17 | 18 | type attestOptions struct { 19 | attach bool 20 | sign bool 21 | } 22 | 23 | func addAttest(parentCmd *cobra.Command) { 24 | opts := attestOptions{} 25 | generateCmd := &cobra.Command{ 26 | Short: fmt.Sprintf("%s attest: generate a VEX attestation", appname), 27 | Long: ``, 28 | Use: "attest", 29 | SilenceUsage: false, 30 | SilenceErrors: false, 31 | // PersistentPreRunE: initLogging, 32 | RunE: func(cmd *cobra.Command, args []string) error { 33 | if len(args) < 2 { 34 | return errors.New("not enough arguments") 35 | } 36 | cmd.SilenceUsage = true 37 | 38 | ctx := context.Background() 39 | 40 | vexctl := ctl.New() 41 | vexctl.Options.Sign = opts.sign 42 | 43 | attestation, err := vexctl.Attest(args[0], args[1:]) 44 | if err != nil { 45 | return fmt.Errorf("generating attestation: %w", err) 46 | } 47 | 48 | if opts.attach { 49 | if err := vexctl.Attach(ctx, attestation, args[1:]); err != nil { 50 | return fmt.Errorf("attaching attestation: %w", err) 51 | } 52 | } 53 | 54 | if err := attestation.ToJSON(os.Stdout); err != nil { 55 | return fmt.Errorf("marshaling attestation to json") 56 | } 57 | 58 | return nil 59 | }, 60 | } 61 | 62 | generateCmd.PersistentFlags().BoolVar( 63 | &opts.attach, 64 | "attach", 65 | true, 66 | "attach the generated attestation to an image", 67 | ) 68 | 69 | generateCmd.PersistentFlags().BoolVar( 70 | &opts.sign, 71 | "sign", 72 | true, 73 | "sign the attestation with sigstore", 74 | ) 75 | 76 | parentCmd.AddCommand(generateCmd) 77 | } 78 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | project_name: vex 2 | 3 | env: 4 | - GO111MODULE=on 5 | - COSIGN_EXPERIMENTAL=true 6 | 7 | before: 8 | hooks: 9 | - go mod tidy 10 | - /bin/bash -c 'if [ -n "$(git --no-pager diff --exit-code go.mod go.sum)" ]; then exit 1; fi' 11 | 12 | gomod: 13 | proxy: true 14 | 15 | builds: 16 | - id: binaries 17 | binary: vex-{{ .Os }}-{{ .Arch }} 18 | no_unique_dist_dir: true 19 | main: . 20 | flags: 21 | - -trimpath 22 | mod_timestamp: '{{ .CommitTimestamp }}' 23 | goos: 24 | - linux 25 | - darwin 26 | - windows 27 | goarch: 28 | - amd64 29 | - arm64 30 | - arm 31 | - s390x 32 | - ppc64le 33 | goarm: 34 | - '7' 35 | ignore: 36 | - goos: windows 37 | goarch: arm64 38 | - goos: windows 39 | goarch: arm 40 | - goos: windows 41 | goarch: s390x 42 | - goos: windows 43 | goarch: ppc64le 44 | ldflags: 45 | - "{{ .Env.LDFLAGS }}" 46 | env: 47 | - CGO_ENABLED=0 48 | 49 | signs: 50 | # Keyless 51 | - id: binary-keyless 52 | signature: "${artifact}.sig" 53 | certificate: "${artifact}.pem" 54 | cmd: cosign 55 | args: ["sign-blob", "--output-signature", "${artifact}.sig", "--output-certificate", "${artifact}.pem", "${artifact}"] 56 | artifacts: binary 57 | - id: checksum-keyless 58 | signature: "${artifact}.sig" 59 | certificate: "${artifact}.pem" 60 | cmd: cosign 61 | args: ["sign-blob", "--output-signature", "${artifact}.sig", "--output-certificate", "${artifact}.pem", "${artifact}"] 62 | artifacts: checksum 63 | 64 | archives: 65 | - format: binary 66 | name_template: "{{ .Binary }}" 67 | allow_different_binary_count: true 68 | 69 | checksum: 70 | name_template: "{{ .ProjectName }}_checksums.txt" 71 | 72 | snapshot: 73 | name_template: SNAPSHOT-{{ .ShortCommit }} 74 | 75 | release: 76 | prerelease: allow 77 | draft: true # allow for manual edits 78 | -------------------------------------------------------------------------------- /internal/cmd/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package cmd 7 | 8 | import ( 9 | "fmt" 10 | 11 | "github.com/sirupsen/logrus" 12 | "github.com/spf13/cobra" 13 | "sigs.k8s.io/release-utils/log" 14 | "sigs.k8s.io/release-utils/version" 15 | ) 16 | 17 | const appname = "vexctl" 18 | 19 | var rootCmd = &cobra.Command{ 20 | Short: "A tool for working with VEX data", 21 | Long: `A tool for working with VEX data 22 | 23 | vexctl is a tool to work with VEX (Vulnerability Exploitability eXchange) 24 | data and to use it to interpret security scanner results. 25 | 26 | It enables users to attach vex information to container images and to 27 | filter result sets using the VEX information to get a clear view of which 28 | vulnerabilities apply to their project. 29 | 30 | For more information see the --attest and --filter subcomands 31 | 32 | `, 33 | Use: appname, 34 | SilenceUsage: false, 35 | PersistentPreRunE: initLogging, 36 | } 37 | 38 | type commandLineOptions struct { 39 | logLevel string 40 | } 41 | 42 | var commandLineOpts = commandLineOptions{} 43 | 44 | func init() { 45 | rootCmd.PersistentFlags().StringVar( 46 | &commandLineOpts.logLevel, 47 | "log-level", 48 | "info", 49 | fmt.Sprintf("the logging verbosity, either %s", log.LevelNames()), 50 | ) 51 | 52 | addFilter(rootCmd) 53 | addAttest(rootCmd) 54 | addMerge(rootCmd) 55 | addCreate(rootCmd) 56 | rootCmd.AddCommand(version.WithFont("doom")) 57 | } 58 | 59 | type vexDocOptions struct { 60 | DocumentID string 61 | Author string 62 | AuthorRole string 63 | } 64 | 65 | type vexStatementOptions struct { 66 | Status string 67 | StatusNotes string 68 | Justification string 69 | ImpactStatement string 70 | Vulnerability string 71 | ActionStatement string 72 | Products []string 73 | Subcomponents []string 74 | } 75 | 76 | func initLogging(*cobra.Command, []string) error { 77 | return log.SetupGlobalLogger(commandLineOpts.logLevel) 78 | } 79 | 80 | // Execute builds the command 81 | func Execute() { 82 | if err := rootCmd.Execute(); err != nil { 83 | logrus.Fatal(err) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /pkg/ctl/ctl_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package ctl 7 | 8 | import ( 9 | "context" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | 14 | "chainguard.dev/vex/pkg/sarif" 15 | "chainguard.dev/vex/pkg/vex" 16 | ) 17 | 18 | func TestVexReport(t *testing.T) { 19 | vexDoc, err := vex.OpenJSON("testdata/test.vex.json") 20 | require.NoError(t, err) 21 | require.NotNil(t, vexDoc) 22 | require.Len(t, vexDoc.Statements, 2) 23 | 24 | report, err := sarif.Open("testdata/nginx.sarif.json") 25 | require.NoError(t, err) 26 | require.NotNil(t, report) 27 | require.Len(t, report.Runs, 1) 28 | require.Len(t, report.Runs[0].Results, 123) 29 | 30 | impl := defaultVexCtlImplementation{} 31 | newReport, err := impl.ApplySingleVEX(report, vexDoc) 32 | require.NoError(t, err) 33 | require.Len(t, newReport.Runs, 1) 34 | require.Len(t, newReport.Runs[0].Results, 122) 35 | } 36 | 37 | func TestMerge(t *testing.T) { 38 | ctx := context.Background() 39 | doc1, err := vex.Load("testdata/document1.vex.json") 40 | require.NoError(t, err) 41 | doc2, err := vex.Load("testdata/document1.vex.json") 42 | require.NoError(t, err) 43 | 44 | impl := defaultVexCtlImplementation{} 45 | for _, tc := range []struct { 46 | opts MergeOptions 47 | docs []*vex.VEX 48 | expectedDoc *vex.VEX 49 | shouldErr bool 50 | }{ 51 | // Zero docs should fail 52 | { 53 | opts: MergeOptions{}, 54 | docs: []*vex.VEX{}, 55 | expectedDoc: &vex.VEX{}, 56 | shouldErr: true, 57 | }, 58 | // One doc results in the same doc 59 | { 60 | opts: MergeOptions{}, 61 | docs: []*vex.VEX{doc1}, 62 | expectedDoc: doc1, 63 | shouldErr: false, 64 | }, 65 | // Two docs, as they are 66 | { 67 | opts: MergeOptions{}, 68 | docs: []*vex.VEX{doc1, doc2}, 69 | expectedDoc: &vex.VEX{ 70 | Metadata: vex.Metadata{}, 71 | Statements: []vex.Statement{ 72 | doc1.Statements[0], 73 | doc2.Statements[0], 74 | }, 75 | }, 76 | shouldErr: false, 77 | }, 78 | } { 79 | doc, err := impl.Merge(ctx, &tc.opts, tc.docs) 80 | if tc.shouldErr { 81 | require.Error(t, err) 82 | continue 83 | } 84 | 85 | // Check doc 86 | require.Len(t, doc.Statements, len(tc.expectedDoc.Statements)) 87 | require.Equal(t, doc.Statements, tc.expectedDoc.Statements) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /pkg/vex/testdata/csaf.json: -------------------------------------------------------------------------------- 1 | { 2 | "document": { 3 | "category": "csaf_vex", 4 | "csaf_version": "2.0", 5 | "notes": [ 6 | { 7 | "category": "summary", 8 | "text": "Example VEX document.", 9 | "title": "Document Title" 10 | } 11 | ], 12 | "publisher": { 13 | "category": "vendor", 14 | "name": "Example Company", 15 | "namespace": "https://psirt.example.com" 16 | }, 17 | "title": "Example VEX Document Use Case 1 - Not Affected", 18 | "tracking": { 19 | "current_release_date": "2022-03-03T11:00:00.000Z", 20 | "generator": { 21 | "date": "2022-03-03T11:00:00.000Z", 22 | "engine": { 23 | "name": "Secvisogram", 24 | "version": "1.11.0" 25 | } 26 | }, 27 | "id": "2022-EVD-UC-01-NA-001", 28 | "initial_release_date": "2022-03-03T11:00:00.000Z", 29 | "revision_history": [ 30 | { 31 | "date": "2022-03-03T11:00:00.000Z", 32 | "number": "1", 33 | "summary": "Initial version." 34 | } 35 | ], 36 | "status": "final", 37 | "version": "1" 38 | } 39 | }, 40 | "product_tree": { 41 | "branches": [ 42 | { 43 | "branches": [ 44 | { 45 | "branches": [ 46 | { 47 | "category": "product_version", 48 | "name": "4.2", 49 | "product": { 50 | "name": "Example Company ABC 4.2", 51 | "product_id": "CSAFPID-0001" 52 | } 53 | } 54 | ], 55 | "category": "product_name", 56 | "name": "ABC" 57 | } 58 | ], 59 | "category": "vendor", 60 | "name": "Example Company" 61 | } 62 | ] 63 | }, 64 | "vulnerabilities": [ 65 | { 66 | "cve": "CVE-2009-4487", 67 | "notes": [ 68 | { 69 | "category": "description", 70 | "text": "nginx 0.7.64 writes data to a log file without sanitizing non-printable characters, which might allow remote attackers to modify a window's title, or possibly execute arbitrary commands or overwrite files, via an HTTP request containing an escape sequence for a terminal emulator.", 71 | "title": "CVE description" 72 | } 73 | ], 74 | "product_status": { 75 | "known_not_affected": [ 76 | "CSAFPID-0001" 77 | ] 78 | }, 79 | "threats": [ 80 | { 81 | "category": "impact", 82 | "details": "Class with vulnerable code was removed before shipping.", 83 | "product_ids": [ 84 | "CSAFPID-0001" 85 | ] 86 | } 87 | ] 88 | } 89 | ] 90 | } -------------------------------------------------------------------------------- /internal/cmd/merge.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package cmd 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | "os" 12 | 13 | "github.com/spf13/cobra" 14 | 15 | "chainguard.dev/vex/pkg/ctl" 16 | "chainguard.dev/vex/pkg/vex" 17 | ) 18 | 19 | type mergeOptions struct { 20 | ctl.MergeOptions 21 | } 22 | 23 | func addMerge(parentCmd *cobra.Command) { 24 | opts := mergeOptions{} 25 | mergeCmd := &cobra.Command{ 26 | Short: fmt.Sprintf("%s merge: merges two or more VEX documents into one", appname), 27 | Long: fmt.Sprintf(`%s merge: merge one or more documents into one 28 | 29 | When composing VEX data out of multiple sources it may be necessary to mix 30 | all statements into a single doc. The merge subcommand mixes the statements 31 | from one or more vex documents into a single, new one. 32 | 33 | Examples: 34 | 35 | # Merge two documents into one 36 | %s merge document1.vex.json document2.vex.json > new.vex.json 37 | 38 | # Merge two documents into one, but only one product 39 | %s merge --product="pkg:apk/wolfi/bash@1.0" document1.vex.json document2.vex.json 40 | 41 | # Merge vulnerability data from two documents into one 42 | %s merge --vulnerability=CVE-2022-3294 document1.vex.json document2.vex.json 43 | 44 | `, appname, appname, appname, appname), 45 | Use: "merge", 46 | SilenceUsage: false, 47 | SilenceErrors: false, 48 | PersistentPreRunE: initLogging, 49 | RunE: func(cmd *cobra.Command, args []string) error { 50 | vexctl := ctl.New() 51 | newVex, err := vexctl.MergeFiles(context.Background(), &opts.MergeOptions, args) 52 | if err != nil { 53 | return fmt.Errorf("merging documents: %w", err) 54 | } 55 | if err := newVex.ToJSON(os.Stdout); err != nil { 56 | return fmt.Errorf("writing new vex document: %w", err) 57 | } 58 | return nil 59 | }, 60 | } 61 | 62 | mergeCmd.PersistentFlags().StringVar( 63 | &opts.DocumentID, 64 | "docid", 65 | "", 66 | "ID for the new VEX document (default will be computed)", 67 | ) 68 | 69 | mergeCmd.PersistentFlags().StringVar( 70 | &opts.Author, 71 | "author", 72 | vex.DefaultAuthor, 73 | "author to record in the new document", 74 | ) 75 | 76 | mergeCmd.PersistentFlags().StringVar( 77 | &opts.AuthorRole, 78 | "author-role", 79 | vex.DefaultRole, 80 | "author role to record in the new document", 81 | ) 82 | 83 | mergeCmd.PersistentFlags().StringSliceVar( 84 | &opts.Vulnerabilities, 85 | "vuln", 86 | []string{}, 87 | "list of vulnerabilities to extract", 88 | ) 89 | 90 | mergeCmd.PersistentFlags().StringSliceVar( 91 | &opts.Products, 92 | "product", 93 | []string{}, 94 | "list of products to merge, all others will be ignored", 95 | ) 96 | 97 | parentCmd.AddCommand(mergeCmd) 98 | } 99 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NFS 2 | .nfs* 3 | 4 | # OSX leaves these everywhere on SMB shares 5 | ._* 6 | 7 | # OSX trash 8 | .DS_Store 9 | 10 | # Eclipse files 11 | .classpath 12 | .project 13 | .settings/** 14 | 15 | # Files generated by JetBrains IDEs, e.g. IntelliJ IDEA 16 | .idea/ 17 | *.iml 18 | 19 | # Vscode files 20 | .vscode 21 | 22 | # This is where the result of the go build goes 23 | /output*/ 24 | /_output*/ 25 | /_output 26 | 27 | # Emacs save files 28 | *~ 29 | \#*\# 30 | .\#* 31 | 32 | # Vim-related files 33 | [._]*.s[a-w][a-z] 34 | [._]s[a-w][a-z] 35 | *.un~ 36 | Session.vim 37 | .netrwhist 38 | 39 | # Go test binaries 40 | *.test 41 | /hack/.test-cmd-auth 42 | 43 | # JUnit test output from ginkgo e2e tests 44 | /junit*.xml 45 | 46 | # Mercurial files 47 | **/.hg 48 | **/.hg* 49 | 50 | # Vagrant 51 | .vagrant 52 | network_closure.sh 53 | 54 | # Local cluster env variables 55 | /cluster/env.sh 56 | 57 | # Compiled binaries in third_party 58 | /third_party/pkg 59 | 60 | # Also ignore etcd installed by hack/install-etcd.sh 61 | /third_party/etcd* 62 | 63 | # User cluster configs 64 | .kubeconfig 65 | 66 | .tags* 67 | 68 | # Version file for dockerized build 69 | .dockerized-kube-version-defs 70 | 71 | # Web UI 72 | /www/master/node_modules/ 73 | /www/master/npm-debug.log 74 | /www/master/shared/config/development.json 75 | 76 | # Karma output 77 | /www/test_out 78 | 79 | # precommit temporary directories created by ./hack/verify-generated-docs.sh and ./hack/lib/util.sh 80 | /_tmp/ 81 | /doc_tmp/ 82 | 83 | # Test artifacts produced by Jenkins jobs 84 | /_artifacts/ 85 | 86 | # Go dependencies installed on Jenkins 87 | /_gopath/ 88 | 89 | # Config directories created by gcloud and gsutil on Jenkins 90 | /.config/gcloud*/ 91 | /.gsutil/ 92 | 93 | # CoreOS stuff 94 | /cluster/libvirt-coreos/coreos_*.img 95 | 96 | # Juju Stuff 97 | /cluster/juju/charms/* 98 | /cluster/juju/bundles/local.yaml 99 | 100 | # Downloaded Kubernetes binary release 101 | /kubernetes/ 102 | 103 | # direnv .envrc files 104 | .envrc 105 | 106 | # Downloaded kubernetes binary release tar ball 107 | kubernetes.tar.gz 108 | 109 | # generated files in any directory 110 | # TODO(thockin): uncomment this when we stop committing the generated files. 111 | #zz_generated.* 112 | 113 | # make-related metadata 114 | /.make/ 115 | # Just in time generated data in the source, should never be committed 116 | /test/e2e/generated/bindata.go 117 | 118 | # This file used by some vendor repos (e.g. github.com/go-openapi/...) to store secret variables and should not be ignored 119 | !\.drone\.sec 120 | 121 | /bazel-* 122 | 123 | # vendored go modules 124 | /vendor 125 | 126 | # git merge conflict originals 127 | *.orig 128 | 129 | # go coverage files 130 | coverage.* 131 | 132 | # test files 133 | tmp 134 | CHANGELOG-*.html 135 | 136 | # downloaded and built binaries 137 | bin 138 | qemu-*-static 139 | rootfs.tar 140 | vexctl 141 | dist/** 142 | -------------------------------------------------------------------------------- /pkg/csaf/testdata/csaf.json: -------------------------------------------------------------------------------- 1 | { 2 | "document": { 3 | "category": "csaf_vex", 4 | "csaf_version": "2.0", 5 | "notes": [ 6 | { 7 | "category": "summary", 8 | "text": "Example VEX document.", 9 | "title": "Document Title" 10 | } 11 | ], 12 | "publisher": { 13 | "category": "vendor", 14 | "name": "Example Company", 15 | "namespace": "https://psirt.example.com" 16 | }, 17 | "title": "Example VEX Document", 18 | "tracking": { 19 | "current_release_date": "2022-03-03T11:00:00.000Z", 20 | "generator": { 21 | "date": "2022-03-03T11:00:00.000Z", 22 | "engine": { 23 | "name": "Secvisogram", 24 | "version": "1.11.0" 25 | } 26 | }, 27 | "id": "2022-EVD-UC-01-NA-001", 28 | "initial_release_date": "2022-03-03T11:00:00.000Z", 29 | "revision_history": [ 30 | { 31 | "date": "2022-03-03T11:00:00.000Z", 32 | "number": "1", 33 | "summary": "Initial version." 34 | } 35 | ], 36 | "status": "final", 37 | "version": "1" 38 | } 39 | }, 40 | "product_tree": { 41 | "branches": [ 42 | { 43 | "branches": [ 44 | { 45 | "product": { 46 | "name": "Example Company ABC 4.2", 47 | "product_id": "CSAFPID-0001", 48 | "product_identification_helper": { 49 | "purl": "pkg:maven/@1.3.4" 50 | } 51 | }, 52 | "branches": [ 53 | { 54 | "category": "product_version", 55 | "name": "4.2", 56 | "product": { 57 | "name": "Example Company ABC 4.2", 58 | "product_id": "INTERNAL-0001", 59 | "product_identification_helper": { 60 | "purl": "pkg:golang/github.com/go-homedir@v1.1.0" 61 | } 62 | } 63 | } 64 | ], 65 | "category": "product_name", 66 | "name": "ABC" 67 | } 68 | ], 69 | "category": "vendor", 70 | "name": "Example Company" 71 | } 72 | ] 73 | }, 74 | "vulnerabilities": [ 75 | { 76 | "cve": "CVE-2009-4487", 77 | "notes": [ 78 | { 79 | "category": "description", 80 | "text": "nginx 0.7.64 writes data to a log file without sanitizing non-printable characters, which might allow remote attackers to modify a window's title, or possibly execute arbitrary commands or overwrite files, via an HTTP request containing an escape sequence for a terminal emulator.", 81 | "title": "CVE description" 82 | } 83 | ], 84 | "product_status": { 85 | "known_not_affected": [ 86 | "CSAFPID-0001" 87 | ] 88 | }, 89 | "threats": [ 90 | { 91 | "category": "impact", 92 | "details": "Class with vulnerable code was removed before shipping.", 93 | "product_ids": [ 94 | "CSAFPID-0001" 95 | ] 96 | } 97 | ] 98 | } 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /pkg/csaf/csaf.go: -------------------------------------------------------------------------------- 1 | package csaf 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "time" 8 | ) 9 | 10 | type CSAF struct { 11 | Document DocumentMetadata `json:"document"` 12 | ProductTree ProductBranch `json:"product_tree"` 13 | Vulnerabilities []Vulnerability `json:"vulnerabilities"` 14 | } 15 | 16 | type DocumentMetadata struct { 17 | Title string `json:"title"` 18 | Tracking Tracking `json:"tracking"` 19 | } 20 | 21 | type Tracking struct { 22 | ID string `json:"id"` 23 | CurrentReleaseDate time.Time `json:"current_release_date"` 24 | } 25 | 26 | type Vulnerability struct { 27 | CVE string `json:"cve"` 28 | ProductStatus map[string][]string `json:"product_status"` 29 | Threats []ThreatData `json:"threats"` 30 | } 31 | 32 | type ThreatData struct { 33 | Category string `json:"category"` 34 | Details string `json:"details"` 35 | ProductIDs []string `json:"product_ids"` 36 | } 37 | 38 | type ProductBranch struct { 39 | Category string `json:"category"` 40 | Name string `json:"name"` 41 | Branches []ProductBranch `json:"branches"` 42 | Product Product `json:"product,omitempty"` 43 | } 44 | 45 | type Product struct { 46 | Name string `json:"name"` 47 | ID string `json:"product_id"` 48 | IdentificationHelper map[string]string `json:"product_identification_helper"` 49 | } 50 | 51 | func Open(path string) (*CSAF, error) { 52 | data, err := os.ReadFile(path) 53 | if err != nil { 54 | return nil, fmt.Errorf("opening CSAF document: %w", err) 55 | } 56 | 57 | csafDoc := &CSAF{} 58 | if err := json.Unmarshal(data, csafDoc); err != nil { 59 | return nil, fmt.Errorf("unmarshalling CSAF document: %w", err) 60 | } 61 | return csafDoc, nil 62 | } 63 | 64 | func (csafDoc *CSAF) FirstProductName() string { 65 | return csafDoc.ProductTree.FindFirstProduct() 66 | } 67 | 68 | // FindFirstProduct recursively searches for the first product in the tree 69 | func (branch *ProductBranch) FindFirstProduct() string { 70 | if branch.Product.ID != "" { 71 | return branch.Product.ID 72 | } 73 | 74 | // No noested branches 75 | if branch.Branches == nil { 76 | return "" 77 | } 78 | 79 | for _, b := range branch.Branches { 80 | if p := b.FindFirstProduct(); p != "" { 81 | return p 82 | } 83 | } 84 | 85 | return "" 86 | } 87 | 88 | // FindFirstProduct recursively searches for the first product in the tree 89 | func (branch *ProductBranch) FindProductIdentifier(helperType, helperValue string) *Product { 90 | if len(branch.Product.IdentificationHelper) != 0 { 91 | for k := range branch.Product.IdentificationHelper { 92 | if k != helperType { 93 | continue 94 | } 95 | if branch.Product.IdentificationHelper[k] == helperValue { 96 | return &branch.Product 97 | } 98 | } 99 | } 100 | 101 | // No noested branches 102 | if branch.Branches == nil { 103 | return nil 104 | } 105 | 106 | for _, b := range branch.Branches { 107 | if p := b.FindProductIdentifier(helperType, helperValue); p != nil { 108 | return p 109 | } 110 | } 111 | 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /pkg/vex/justification.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package vex 7 | 8 | // Justification describes why a given component is not affected by a 9 | // vulnerability. 10 | type Justification string 11 | 12 | const ( 13 | // ComponentNotPresent means the vulnerable component is not included in the artifact. 14 | // 15 | // ComponentNotPresent is a strong justification that the artifact is not affected. 16 | ComponentNotPresent Justification = "component_not_present" 17 | 18 | // VulnerableCodeNotPresent means the vulnerable component is included in 19 | // artifact, but the vulnerable code is not present. Typically, this case occurs 20 | // when source code is configured or built in a way that excluded the vulnerable 21 | // code. 22 | // 23 | // VulnerableCodeNotPresent is a strong justification that the artifact is not affected. 24 | VulnerableCodeNotPresent Justification = "vulnerable_code_not_present" 25 | 26 | // VulnerableCodeNotInExecutePath means the vulnerable code (likely in 27 | // [subcomponent_id]) can not be executed as it is used by [product_id]. 28 | // Typically, this case occurs when [product_id] includes the vulnerable 29 | // [subcomponent_id] and the vulnerable code but does not call or use the 30 | // vulnerable code. 31 | VulnerableCodeNotInExecutePath Justification = "vulnerable_code_not_in_execute_path" 32 | 33 | // VulnerableCodeCannotBeControlledByAdversary means the vulnerable code cannot 34 | // be controlled by an attacker to exploit the vulnerability. 35 | // 36 | // This justification could be difficult to prove conclusively. 37 | VulnerableCodeCannotBeControlledByAdversary Justification = "vulnerable_code_cannot_be_controlled_by_adversary" 38 | 39 | // InlineMitigationsAlreadyExist means [product_id] includes built-in protections 40 | // or features that prevent exploitation of the vulnerability. These built-in 41 | // protections cannot be subverted by the attacker and cannot be configured or 42 | // disabled by the user. These mitigations completely prevent exploitation based 43 | // on known attack vectors. 44 | // 45 | // This justification could be difficult to prove conclusively. History is 46 | // littered with examples of mitigation bypasses, typically involving minor 47 | // modifications of existing exploit code. 48 | InlineMitigationsAlreadyExist Justification = "inline_mitigations_already_exist" 49 | ) 50 | 51 | // Justifications returns a list of the valid Justification values. 52 | func Justifications() []string { 53 | return []string{ 54 | string(ComponentNotPresent), 55 | string(VulnerableCodeNotPresent), 56 | string(VulnerableCodeNotInExecutePath), 57 | string(VulnerableCodeCannotBeControlledByAdversary), 58 | string(InlineMitigationsAlreadyExist), 59 | } 60 | } 61 | 62 | // Valid returns a bool indicating whether the Justification value is equal to 63 | // one of the enumerated allowed values for Justification. 64 | func (j Justification) Valid() bool { 65 | switch j { 66 | case ComponentNotPresent, 67 | VulnerableCodeNotPresent, 68 | VulnerableCodeNotInExecutePath, 69 | VulnerableCodeCannotBeControlledByAdversary, 70 | InlineMitigationsAlreadyExist: 71 | 72 | return true 73 | 74 | default: 75 | 76 | return false 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pkg/attestation/attestation.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package attestation 7 | 8 | import ( 9 | "bytes" 10 | "context" 11 | "encoding/json" 12 | "fmt" 13 | "io" 14 | "strings" 15 | "time" 16 | 17 | "github.com/google/go-containerregistry/pkg/crane" 18 | intoto "github.com/in-toto/in-toto-golang/in_toto" 19 | "github.com/sigstore/cosign/cmd/cosign/cli/options" 20 | "github.com/sigstore/cosign/cmd/cosign/cli/sign" 21 | "github.com/sigstore/sigstore/pkg/signature/dsse" 22 | signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" 23 | 24 | "chainguard.dev/vex/pkg/vex" 25 | ) 26 | 27 | type Attestation struct { 28 | intoto.StatementHeader 29 | // Predicate contains type specific metadata. 30 | Predicate vex.VEX `json:"predicate"` 31 | Signed bool `json:"-"` 32 | signedData []byte `json:"-"` 33 | } 34 | 35 | func New() *Attestation { 36 | return &Attestation{ 37 | StatementHeader: intoto.StatementHeader{ 38 | Type: intoto.StatementInTotoV01, 39 | PredicateType: vex.TypeURI, 40 | Subject: []intoto.Subject{}, 41 | }, 42 | Predicate: vex.New(), 43 | } 44 | } 45 | 46 | // ToJSON returns the attestation as a JSON byte array 47 | func (att *Attestation) ToJSON(w io.Writer) error { 48 | if att.Signed { 49 | if _, err := w.Write(att.signedData); err != nil { 50 | return fmt.Errorf("writing signed attestation") 51 | } 52 | return nil 53 | } 54 | 55 | enc := json.NewEncoder(w) 56 | enc.SetIndent("", " ") 57 | enc.SetEscapeHTML(false) 58 | 59 | if err := enc.Encode(att); err != nil { 60 | return fmt.Errorf("encoding attestation: %w", err) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (att *Attestation) AddImageSubjects(imageRefs []string) error { 67 | for _, refString := range imageRefs { 68 | digest, err := crane.Digest(refString) 69 | if err != nil { 70 | return fmt.Errorf("getting image digest: %w", err) 71 | } 72 | s := intoto.Subject{ 73 | Name: refString, 74 | Digest: map[string]string{"sha256": strings.TrimPrefix(digest, "sha256:")}, 75 | } 76 | 77 | att.Subject = append(att.Subject, s) 78 | } 79 | return nil 80 | } 81 | 82 | // Sign the attestation 83 | func (att *Attestation) Sign() error { 84 | ctx := context.Background() 85 | var timeout time.Duration /// TODO move to options 86 | var certPath, certChainPath string 87 | ko := options.KeyOpts{ 88 | // KeyRef: s.options.PrivateKeyPath, 89 | // IDToken: identityToken, 90 | FulcioURL: options.DefaultFulcioURL, 91 | RekorURL: options.DefaultRekorURL, 92 | OIDCIssuer: options.DefaultOIDCIssuerURL, 93 | OIDCClientID: "sigstore", 94 | 95 | InsecureSkipFulcioVerify: false, 96 | SkipConfirmation: true, 97 | // FulcioAuthFlow: "", 98 | } 99 | 100 | if timeout != 0 { 101 | var cancelFn context.CancelFunc 102 | ctx, cancelFn = context.WithTimeout(ctx, timeout) 103 | defer cancelFn() 104 | } 105 | 106 | sv, err := sign.SignerFromKeyOpts(ctx, certPath, certChainPath, ko) 107 | if err != nil { 108 | return fmt.Errorf("getting signer: %w", err) 109 | } 110 | defer sv.Close() 111 | 112 | // Wrap the attestation in the DSSE envelope 113 | wrapped := dsse.WrapSigner(sv, "application/vnd.in-toto+json") 114 | 115 | var b bytes.Buffer 116 | if err := att.ToJSON(&b); err != nil { 117 | return fmt.Errorf("serializing attestation to json: %w", err) 118 | } 119 | 120 | signedPayload, err := wrapped.SignMessage( 121 | bytes.NewReader(b.Bytes()), signatureoptions.WithContext(ctx), 122 | ) 123 | if err != nil { 124 | return fmt.Errorf("signing attestation: %w", err) 125 | } 126 | 127 | att.signedData = signedPayload 128 | att.Signed = true 129 | return nil 130 | } 131 | -------------------------------------------------------------------------------- /internal/cmd/filter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package cmd 7 | 8 | import ( 9 | "context" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "os" 14 | 15 | "github.com/spf13/cobra" 16 | 17 | "chainguard.dev/vex/pkg/ctl" 18 | "chainguard.dev/vex/pkg/sarif" 19 | "chainguard.dev/vex/pkg/vex" 20 | ) 21 | 22 | type filterOptions struct { 23 | reportFormat string 24 | products []string 25 | } 26 | 27 | func (o *filterOptions) Validate() error { 28 | if o.reportFormat != "vex" && o.reportFormat != "csaf" && o.reportFormat != "cyclonedx" { 29 | return errors.New("invalid vex document format (must be one of vex, cyclonedx or csaf)") 30 | } 31 | return nil 32 | } 33 | 34 | func addFilter(parentCmd *cobra.Command) { 35 | opts := filterOptions{} 36 | filterCmd := &cobra.Command{ 37 | Short: fmt.Sprintf("%s filter: apply a vex document to a results set", appname), 38 | Long: fmt.Sprintf(`%s filter: apply a vex document to a results set 39 | 40 | When using the filter subcommand, %s will read a scanner results file 41 | and apply one or more VEX files to the results. The output will be 42 | the same results file with the VEX'ed vulnerabilities removed. 43 | 44 | Examples: 45 | 46 | # VEX a SARIF report from vex files: 47 | vexctl filter myreport.sarif.json data1.vex.json data2.vex.json 48 | 49 | # VEX a SARIF report from an atestation in an image: 50 | vexctl filter myreport.sarif.json cgr.dev/image@sha256:e4cf37d568d195b4b5af4c3..... 51 | 52 | VEX information can be read from CSAF, CycloneDX or our own simpler VEX 53 | format. 54 | 55 | It can also be read from an attestation attached to a container image. 56 | 57 | When dealing with CSAF files, you can specify which of the products in the 58 | document should be VEX'ed by specifying --product=PRODUCT_ID. 59 | 60 | 61 | `, appname, appname), 62 | Use: "filter", 63 | SilenceUsage: false, 64 | SilenceErrors: false, 65 | PersistentPreRunE: initLogging, 66 | RunE: func(cmd *cobra.Command, args []string) error { 67 | if len(args) < 2 { 68 | fmt.Println(cmd.Long) 69 | return errors.New("not enough arguments") 70 | } 71 | if err := opts.Validate(); err != nil { 72 | return fmt.Errorf("validating options: %w", err) 73 | } 74 | 75 | ctx := context.Background() 76 | vexctl := ctl.New() 77 | vexctl.Options.Products = opts.products 78 | vexctl.Options.Format = opts.reportFormat 79 | 80 | // TODO: Autodetect piped stdin 81 | reportFileName := args[0] 82 | if args[0] == "-" { 83 | tmp, err := os.CreateTemp("", "tmp-*.sarif.json") 84 | if err != nil { 85 | return fmt.Errorf("creating temp sarif file") 86 | } 87 | defer os.Remove(tmp.Name()) 88 | if _, err := io.Copy(tmp, os.Stdin); err != nil { 89 | return fmt.Errorf("writing stdin: %w", err) 90 | } 91 | reportFileName = tmp.Name() 92 | } 93 | 94 | // Open all docs 95 | report, err := sarif.Open(reportFileName) 96 | if err != nil { 97 | return fmt.Errorf("opening sarif report") 98 | } 99 | vexes := []*vex.VEX{} 100 | for i := 1; i < len(args); i++ { 101 | doc, err := vexctl.VexFromURI(ctx, args[i]) 102 | if err != nil { 103 | return fmt.Errorf("opening %s: %w", args[i], err) 104 | } 105 | vexes = append(vexes, doc) 106 | } 107 | 108 | report, err = vexctl.Apply(report, vexes) 109 | if err != nil { 110 | return fmt.Errorf("applying vexes to report: %w", err) 111 | } 112 | 113 | return report.ToJSON(os.Stdout) 114 | }, 115 | } 116 | 117 | filterCmd.PersistentFlags().StringVar( 118 | &opts.reportFormat, 119 | "format", 120 | "vex", 121 | "format of the vex document (vex | csaf | cyclonedx)", 122 | ) 123 | 124 | filterCmd.PersistentFlags().StringSliceVar( 125 | &opts.products, 126 | "product", 127 | []string{}, 128 | "IDs of products in a CSAF document to VEX (defaults to first one found)", 129 | ) 130 | 131 | parentCmd.AddCommand(filterCmd) 132 | } 133 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | run: 3 | concurrency: 6 4 | deadline: 5m 5 | issues: 6 | exclude-rules: 7 | # counterfeiter fakes are usually named 'fake_.go' 8 | - path: fake_.*\.go 9 | linters: 10 | - gocritic 11 | - golint 12 | - dupl 13 | 14 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 15 | max-issues-per-linter: 0 16 | 17 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 18 | max-same-issues: 0 19 | linters: 20 | disable-all: true 21 | enable: 22 | - asciicheck 23 | - bodyclose 24 | - depguard 25 | - dogsled 26 | - dupl 27 | - durationcheck 28 | - errcheck 29 | - goconst 30 | - gocritic 31 | - gocyclo 32 | - godox 33 | - gofmt 34 | - gofumpt 35 | - goheader 36 | - goimports 37 | - gomoddirectives 38 | - gomodguard 39 | - goprintffuncname 40 | - gosec 41 | - gosimple 42 | - govet 43 | - importas 44 | - ineffassign 45 | - makezero 46 | - misspell 47 | - nakedret 48 | - nolintlint 49 | - prealloc 50 | - predeclared 51 | - promlinter 52 | - revive 53 | - staticcheck 54 | - stylecheck 55 | - typecheck 56 | - unconvert 57 | - unparam 58 | - unused 59 | - whitespace 60 | # - cyclop 61 | # - errorlint 62 | # - exhaustive 63 | # - exhaustivestruct 64 | # - exportloopref 65 | # - forbidigo 66 | # - forcetypeassert 67 | # - funlen 68 | # - gci 69 | # - gochecknoglobals 70 | # - gochecknoinits 71 | # - gocognit 72 | # - godot 73 | # - goerr113 74 | # - gomnd 75 | # - ifshort 76 | # - lll 77 | # - nestif 78 | # - nilerr 79 | # - nlreturn 80 | # - noctx 81 | # - paralleltest 82 | # - scopelint 83 | # - tagliatelle 84 | # - testpackage 85 | # - thelper 86 | # - tparallel 87 | # - wastedassign 88 | # - wrapcheck 89 | # - wsl 90 | linters-settings: 91 | godox: 92 | keywords: 93 | - BUG 94 | - FIXME 95 | - HACK 96 | errcheck: 97 | check-type-assertions: true 98 | check-blank: true 99 | gocritic: 100 | enabled-checks: 101 | # Diagnostic 102 | - appendAssign 103 | - argOrder 104 | - badCond 105 | - caseOrder 106 | - codegenComment 107 | - commentedOutCode 108 | - deprecatedComment 109 | - dupArg 110 | - dupBranchBody 111 | - dupCase 112 | - dupSubExpr 113 | - exitAfterDefer 114 | - flagDeref 115 | - flagName 116 | - nilValReturn 117 | - offBy1 118 | - sloppyReassign 119 | - weakCond 120 | - octalLiteral 121 | 122 | # Performance 123 | - appendCombine 124 | - equalFold 125 | - hugeParam 126 | - indexAlloc 127 | - rangeExprCopy 128 | - rangeValCopy 129 | 130 | # Style 131 | - assignOp 132 | - boolExprSimplify 133 | - captLocal 134 | - commentFormatting 135 | - commentedOutImport 136 | - defaultCaseOrder 137 | - docStub 138 | - elseif 139 | - emptyFallthrough 140 | - emptyStringTest 141 | - hexLiteral 142 | - methodExprCall 143 | - regexpMust 144 | - singleCaseSwitch 145 | - sloppyLen 146 | - stringXbytes 147 | - switchTrue 148 | - typeAssertChain 149 | - typeSwitchVar 150 | - underef 151 | - unlabelStmt 152 | - unlambda 153 | - unslice 154 | - valSwap 155 | - wrapperFunc 156 | - yodaStyleExpr 157 | # - ifElseChain 158 | 159 | # Opinionated 160 | - builtinShadow 161 | - importShadow 162 | - initClause 163 | - nestingReduce 164 | - paramTypeCombine 165 | - ptrToRefParam 166 | - typeUnparen 167 | - unnamedResult 168 | - unnecessaryBlock 169 | nolintlint: 170 | # Enable to ensure that nolint directives are all used. Default is true. 171 | allow-unused: false 172 | # Disable to ensure that nolint directives don't have a leading space. Default is true. 173 | # TODO(lint): Enforce machine-readable `nolint` directives 174 | allow-leading-space: true 175 | # Exclude following linters from requiring an explanation. Default is []. 176 | allow-no-explanation: [] 177 | # Enable to require an explanation of nonzero length after each nolint directive. Default is false. 178 | # TODO(lint): Enforce explanations for `nolint` directives 179 | require-explanation: false 180 | # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. 181 | require-specific: true 182 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Archival Notice 3 | 4 | This repository has been archived. The work initially done here 5 | grew to become the [OpenVEX project](https://openvex.dev). 6 | 7 | The code originally hosted in this repo is now split into a couple 8 | of repositories in the [OpenVEX GitHub organization](https://github.com/openvex) 9 | like [vexctl](https://github.com/openvex/vexctl), 10 | [go-vex](https://github.com/openvex/go-vex) 11 | and the [OpenVEX spec](https://github.com/openvex/spec). 12 | 13 | Thanks for your support! 14 | 15 | :heart: Chainguard 16 | --- 17 | 18 | 19 | # vexctl: A tool to make VEX work 20 | 21 | `vexctl` is a tool to apply and attest VEX (Vulnerability Exploitability eXchange) 22 | data. Its purpose is to "turn off" alerts of vulnerabilities known not to affect 23 | a product. 24 | 25 | VEX can be though as a "negative security advisory". Using VEX, software authors 26 | can communicate to their users that a vulnerable component has no security 27 | implications for their product. 28 | 29 | ## Operational Model 30 | 31 | To achieve its mission, `vexctl` has two main modes of operation. One 32 | helps the user create VEX statements, the second applies the VEX data 33 | to scanner results. 34 | 35 | ### 1. Create VEX Statements 36 | 37 | VEX data can be created to a file on disk or it can be captured in a 38 | signed attestation which can be attached to a container image. 39 | 40 | The data is generated from a known rule set (the Golden Data) which is 41 | reused and reapplied to new releases of the same project. 42 | 43 | #### Generation Examples 44 | 45 | ``` 46 | # Attest and attach vex statements in mydata.vex.json to a container image: 47 | vexctl attest --attach --sign mydata.vex.json cgr.dev/image@sha256:e4cf37d568d195b4.. 48 | 49 | ``` 50 | 51 | ### 2. VEXing a Results Set 52 | 53 | Using statements in a VEX document or from an attestation, `vexctl` will filter 54 | security scanner results to remove _vexed out_ entries. 55 | 56 | #### Filtering Examples 57 | 58 | ``` 59 | # From a VEX file: 60 | vexctl filter scan_results.sarif.json vex_data.csaf 61 | 62 | 63 | # From a stored VEX attestation: 64 | vexctl filter scan_results.sarif.json cgr.dev/image@sha256:e4cf37d568d195b4b5af4c36a... 65 | 66 | ``` 67 | 68 | The output from both examples willl the same SARIF results data 69 | without those ulnerabilities stated as not explitable: 70 | 71 | ```json 72 | { 73 | "version": "2.1.0", 74 | "$schema": "https://json.schemastore.org/sarif-2.1.0-rtm.5.json", 75 | "runs": [ 76 | { 77 | "tool": { 78 | "driver": { 79 | "fullName": "Trivy Vulnerability Scanner", 80 | "informationUri": "https://github.com/aquasecurity/trivy", 81 | "name": "Trivy", 82 | "rules": [ 83 | 84 | ``` 85 | 86 | We support results files in SARIF for now. We plan to add support for the 87 | propietary formats of the most popular scanners. 88 | 89 | ### Multiple VEX Files 90 | 91 | Assessing impact is process that takes time. VEX is designed to 92 | communicate with users as time progresses. An example timeline may look like 93 | this: 94 | 95 | 1. A project becomes aware of `CVE-2022-12345`, associated with one of its components. 96 | 2. Developers issue a VEX data file with a status of `under_investigation` to 97 | inform their users they are aware of the CVE but are checking what impact it has. 98 | 3. After investigation, the developers determine the CVE has no impact 99 | in their project because the vulnerable function in the component is never executed. 100 | 4. They issue a second VEX document with a status of `not_affected` and using 101 | the `vulnerable_code_not_in_execute_path` justification. 102 | 103 | `vexctl` will read all the documents in cronological order and "replay" the 104 | known impacts statuses the order they were found, effectively computing the 105 | `not_affected` status. 106 | 107 | If a sarif report is VEX'ed with `vexctl` any entries alerting of CVE-2022-12345 108 | will be filtered out. 109 | 110 | ## Build vexctl 111 | 112 | To build `vexctl` clone this repository and run simply run make. 113 | 114 | ```console 115 | git clone git@github.com:chainguard-dev/vex.git 116 | cd vex 117 | make 118 | 119 | ./vexctl version 120 | _ _ _____ __ __ _____ _____ _ 121 | | | | || ___|\ \ / // __ \|_ _|| | 122 | | | | || |__ \ V / | / \/ | | | | 123 | | | | || __| / \ | | | | | | 124 | \ \_/ /| |___ / /^\ \| \__/\ | | | |____ 125 | \___/ \____/ \/ \/ \____/ \_/ \_____/ 126 | vexctl: A tool for working with VEX data 127 | 128 | GitVersion: devel 129 | GitCommit: unknown 130 | GitTreeState: unknown 131 | BuildDate: unknown 132 | GoVersion: go1.19 133 | Compiler: gc 134 | Platform: linux/amd64 135 | ``` 136 | -------------------------------------------------------------------------------- /pkg/ctl/ctl.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package ctl 7 | 8 | import ( 9 | "context" 10 | "fmt" 11 | 12 | "chainguard.dev/vex/pkg/attestation" 13 | "chainguard.dev/vex/pkg/sarif" 14 | "chainguard.dev/vex/pkg/vex" 15 | ) 16 | 17 | type VexCtl struct { 18 | impl Implementation 19 | Options Options 20 | } 21 | 22 | type Options struct { 23 | Products []string // List of products to match in CSAF docs 24 | Format string // Firmat of the vex documents 25 | Sign bool // When true, attestations will be signed before attaching 26 | } 27 | 28 | func New() *VexCtl { 29 | return &VexCtl{ 30 | impl: &defaultVexCtlImplementation{}, 31 | } 32 | } 33 | 34 | // ApplyFiles takes a list of paths to vex files and applies them to a report 35 | func (vexctl *VexCtl) ApplyFiles(r *sarif.Report, files []string) (*sarif.Report, error) { 36 | vexes, err := vexctl.impl.OpenVexData(vexctl.Options, files) 37 | if err != nil { 38 | return nil, fmt.Errorf("opening vex data: %w", err) 39 | } 40 | 41 | return vexctl.Apply(r, vexes) 42 | } 43 | 44 | // Apply takes a sarif report and applies one or more vex documents 45 | func (vexctl *VexCtl) Apply(r *sarif.Report, vexDocs []*vex.VEX) (finalReport *sarif.Report, err error) { 46 | // Sort the docs by date 47 | vexDocs = vexctl.impl.Sort(vexDocs) 48 | 49 | // Apply the sorted documents to the report 50 | for i, doc := range vexDocs { 51 | finalReport, err = vexctl.impl.ApplySingleVEX(r, doc) 52 | if err != nil { 53 | return nil, fmt.Errorf("applying vex document #%d: %w", i, err) 54 | } 55 | } 56 | 57 | return finalReport, nil 58 | } 59 | 60 | // Generate an attestation from a VEX 61 | func (vexctl *VexCtl) Attest(vexDataPath string, imageRefs []string) (*attestation.Attestation, error) { 62 | doc, err := vexctl.impl.OpenVexData(vexctl.Options, []string{vexDataPath}) 63 | if err != nil { 64 | return nil, fmt.Errorf("opening vex data: %w", err) 65 | } 66 | 67 | // Generate the attestation 68 | att := attestation.New() 69 | att.Predicate = *doc[0] 70 | if err := att.AddImageSubjects(imageRefs); err != nil { 71 | return nil, fmt.Errorf("adding image references to attestation") 72 | } 73 | 74 | return att, nil 75 | } 76 | 77 | // Attach attaches an attestation to a list of images 78 | func (vexctl *VexCtl) Attach(ctx context.Context, att *attestation.Attestation, imageRefs []string) (err error) { 79 | // Sign the attestation 80 | if vexctl.Options.Sign { 81 | if err := att.Sign(); err != nil { 82 | return fmt.Errorf("signing attestation: %w", err) 83 | } 84 | } 85 | 86 | for _, ref := range imageRefs { 87 | if err := vexctl.impl.Attach(ctx, att, ref); err != nil { 88 | return fmt.Errorf("attaching attestation: %w", err) 89 | } 90 | } 91 | 92 | return nil 93 | } 94 | 95 | // VexFromURI return a vex doc from a path, image ref or URI 96 | func (vexctl *VexCtl) VexFromURI(ctx context.Context, uri string) (vexData *vex.VEX, err error) { 97 | sourceType, err := vexctl.impl.SourceType(uri) 98 | if err != nil { 99 | return nil, fmt.Errorf("resolving VEX source: %w", err) 100 | } 101 | var vexes []*vex.VEX 102 | switch sourceType { 103 | case "file": 104 | vexes, err = vexctl.impl.OpenVexData(vexctl.Options, []string{uri}) 105 | if err == nil { 106 | vexData = vexes[0] 107 | } 108 | case "image": 109 | vexes, err = vexctl.impl.ReadImageAttestations(ctx, vexctl.Options, uri) 110 | if err == nil { 111 | if len(vexes) == 0 { 112 | return nil, fmt.Errorf("no attestations found in image") 113 | } 114 | vexData = vexes[0] 115 | } 116 | default: 117 | return nil, fmt.Errorf("unable to resolve source type (file or image)") 118 | } 119 | 120 | if err != nil { 121 | return nil, fmt.Errorf("opening vex data from %s: %w", uri, err) 122 | } 123 | return vexData, err 124 | } 125 | 126 | // Merge combines several documents into one 127 | func (vexctl *VexCtl) Merge(ctx context.Context, opts *MergeOptions, vexes []*vex.VEX) (*vex.VEX, error) { 128 | doc, err := vexctl.impl.Merge(ctx, opts, vexes) 129 | if err != nil { 130 | return nil, fmt.Errorf("merging %d documents: %w", len(vexes), err) 131 | } 132 | return doc, nil 133 | } 134 | 135 | // MergeFiles is like Merge but takes filepaths instead of actual VEX documents 136 | func (vexctl *VexCtl) MergeFiles(ctx context.Context, opts *MergeOptions, filePaths []string) (*vex.VEX, error) { 137 | vexes, err := vexctl.impl.LoadFiles(ctx, filePaths) 138 | if err != nil { 139 | return nil, fmt.Errorf("loading files: %w", err) 140 | } 141 | 142 | // Merge'em Dano 143 | doc, err := vexctl.impl.Merge(ctx, opts, vexes) 144 | if err != nil { 145 | return nil, fmt.Errorf("merging %d documents: %w", len(vexes), err) 146 | } 147 | return doc, nil 148 | } 149 | -------------------------------------------------------------------------------- /pkg/vex/vex_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package vex 7 | 8 | import ( 9 | "fmt" 10 | "testing" 11 | "time" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestLoadYAML(t *testing.T) { 17 | vexDoc, err := OpenYAML("testdata/vex.yaml") 18 | require.NoError(t, err) 19 | 20 | require.Len(t, vexDoc.Statements, 2) 21 | } 22 | 23 | func TestLoadCSAF(t *testing.T) { 24 | vexDoc, err := OpenCSAF("testdata/csaf.json", []string{}) 25 | require.NoError(t, err) 26 | require.Len(t, vexDoc.Statements, 1) 27 | require.Equal(t, vexDoc.Statements[0].Vulnerability, "CVE-2009-4487") 28 | require.Equal(t, vexDoc.Statements[0].Status, StatusNotAffected) 29 | require.Equal(t, vexDoc.Metadata.ID, "2022-EVD-UC-01-NA-001") 30 | } 31 | 32 | func genTestDoc(t *testing.T) VEX { 33 | ts, err := time.Parse(time.RFC3339, "2022-12-22T16:36:43-05:00") 34 | require.NoError(t, err) 35 | return VEX{ 36 | Metadata: Metadata{ 37 | Author: "John Doe", 38 | AuthorRole: "VEX Writer Extraordinaire", 39 | Timestamp: &ts, 40 | Version: "1", 41 | Tooling: "OpenVEX", 42 | Supplier: "Chainguard Inc", 43 | }, 44 | Statements: []Statement{ 45 | { 46 | Vulnerability: "CVE-1234-5678", 47 | VulnDescription: "", 48 | Products: []string{"pkg:apk/wolfi/bash@1.0.0"}, 49 | Status: "under_investigation", 50 | }, 51 | }, 52 | } 53 | } 54 | 55 | func TestCanonicalHash(t *testing.T) { 56 | goldenHash := `461bb1de8d85c7a6af96edf24d0e0672726d248500e63c5413f89db0c6710fa0` 57 | 58 | otherTS, err := time.Parse(time.RFC3339, "2019-01-22T16:36:43-05:00") 59 | require.NoError(t, err) 60 | 61 | for i, tc := range []struct { 62 | prepare func(*VEX) 63 | expected string 64 | shouldErr bool 65 | }{ 66 | // Default Expected 67 | {func(v *VEX) {}, goldenHash, false}, 68 | // Adding a statement changes the hash 69 | { 70 | func(v *VEX) { 71 | v.Statements = append(v.Statements, Statement{ 72 | Vulnerability: "CVE-2010-543231", 73 | Products: []string{"pkg:apk/wolfi/git@2.0.0"}, 74 | Status: "affected", 75 | }) 76 | }, 77 | "cf392111c8dfee8f6a115780de1eabf292fcd36aafb6eca75952ea7e2d648e21", 78 | false, 79 | }, 80 | // Changing metadata should not change hash 81 | { 82 | func(v *VEX) { 83 | v.Author = "123" 84 | v.AuthorRole = "abc" 85 | v.ID = "298347" // Mmhh... 86 | v.Supplier = "Mr Supplier" 87 | v.Tooling = "Fake Tool 1.0" 88 | }, 89 | goldenHash, 90 | false, 91 | }, 92 | // Changing other statement metadata should not change the hash 93 | { 94 | func(v *VEX) { 95 | v.Statements[0].ActionStatement = "Action!" 96 | v.Statements[0].VulnDescription = "It is very bad" 97 | v.Statements[0].StatusNotes = "Let's note somthn here" 98 | v.Statements[0].ImpactStatement = "We evaded this CVE by a hair" 99 | v.Statements[0].ActionStatementTimestamp = &otherTS 100 | }, 101 | goldenHash, 102 | false, 103 | }, 104 | // Changing products changes the hash 105 | { 106 | func(v *VEX) { 107 | v.Statements[0].Products[0] = "cool router, bro" 108 | }, 109 | "3ba778366d70b4fc656f9c1338a6be26fab55a7d011db4ceddf2f4840080ab3b", 110 | false, 111 | }, 112 | // Changing document time changes the hash 113 | { 114 | func(v *VEX) { 115 | v.Timestamp = &otherTS 116 | }, 117 | "c69a58b923d83f2c0952a508572aec6529801950e9dcac520dfbcbb953fffe52", 118 | false, 119 | }, 120 | // Same timestamp in statement as doc should not change the hash 121 | { 122 | func(v *VEX) { 123 | v.Statements[0].Timestamp = v.Timestamp 124 | }, 125 | goldenHash, 126 | false, 127 | }, 128 | } { 129 | doc := genTestDoc(t) 130 | tc.prepare(&doc) 131 | hashString, err := doc.CanonicalHash() 132 | if tc.shouldErr { 133 | require.Error(t, err) 134 | } else { 135 | require.NoError(t, err) 136 | } 137 | require.Equal(t, tc.expected, hashString, fmt.Sprintf("Testcase #%d %s", i, doc.Statements[0].Products[0])) 138 | } 139 | } 140 | 141 | func TestGenerateCanonicalID(t *testing.T) { 142 | for _, tc := range []struct { 143 | prepare func(*VEX) 144 | expectedID string 145 | }{ 146 | { 147 | // Normal generation 148 | prepare: func(v *VEX) {}, 149 | expectedID: "https://openvex.dev/docs/public/vex-461bb1de8d85c7a6af96edf24d0e0672726d248500e63c5413f89db0c6710fa0", 150 | }, 151 | { 152 | // Existing IDs should not be changed 153 | prepare: func(v *VEX) { v.ID = "VEX-ID-THAT-ALREADY-EXISTED" }, 154 | expectedID: "VEX-ID-THAT-ALREADY-EXISTED", 155 | }, 156 | } { 157 | doc := genTestDoc(t) 158 | tc.prepare(&doc) 159 | id, err := doc.GenerateCanonicalID() 160 | require.NoError(t, err) 161 | require.Equal(t, tc.expectedID, id) 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /pkg/vex/statement.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package vex 7 | 8 | import ( 9 | "fmt" 10 | "sort" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | // A Statement is a declaration conveying a single [status] for a single [vul_id] for one or more [product_id]s. A VEX Statement exists within a VEX Document. 16 | type Statement struct { 17 | // [vul_id] SHOULD use existing and well known identifiers, for example: 18 | // CVE, the Global Security Database (GSD), or a supplier’s vulnerability 19 | // tracking system. It is expected that vulnerability identification systems 20 | // are external to and maintained separately from VEX. 21 | // 22 | // [vul_id] MAY be URIs or URLs. 23 | // [vul_id] MAY be arbitrary and MAY be created by the VEX statement [author]. 24 | Vulnerability string `json:"vulnerability,omitempty"` 25 | VulnDescription string `json:"vuln_description,omitempty"` 26 | 27 | // Timestamp is the time at which the information expressed in the Statement 28 | // was known to be true. 29 | Timestamp *time.Time `json:"timestamp,omitempty"` 30 | 31 | // ProductIdentifiers 32 | // Product details MUST specify what Status applies to. 33 | // Product details MUST include [product_id] and MAY include [subcomponent_id]. 34 | Products []string `json:"products,omitempty"` 35 | Subcomponents []string `json:"subcomponents,omitempty"` 36 | 37 | // A VEX statement MUST provide Status of the vulnerabilities with respect to the 38 | // products and components listed in the statement. Status MUST be one of the 39 | // Status const values, some of which have further options and requirements. 40 | Status Status `json:"status"` 41 | 42 | // [status_notes] MAY convey information about how [status] was determined 43 | // and MAY reference other VEX information. 44 | StatusNotes string `json:"status_notes,omitempty"` 45 | 46 | // For ”not_affected” status, a VEX statement MUST include a status Justification 47 | // that further explains the status. 48 | Justification Justification `json:"justification,omitempty"` 49 | 50 | // For ”not_affected” status, a VEX statement MAY include an ImpactStatement 51 | // that contains a description why the vulnerability cannot be exploited. 52 | ImpactStatement string `json:"impact_statement,omitempty"` 53 | 54 | // For "affected" status, a VEX statement MUST include an ActionStatement that 55 | // SHOULD describe actions to remediate or mitigate [vul_id]. 56 | ActionStatement string `json:"action_statement,omitempty"` 57 | ActionStatementTimestamp *time.Time `json:"action_statement_timestamp,omitempty"` 58 | } 59 | 60 | // Validate checks to see whether the given Statement is valid. If it's not, an 61 | // error is returned explaining the reason the Statement is invalid. Otherwise, 62 | // nil is returned. 63 | func (stmt Statement) Validate() error { //nolint:gocritic // turning off for rule hugeParam 64 | if s := stmt.Status; !s.Valid() { 65 | return fmt.Errorf("invalid status value %q, must be one of [%s]", s, strings.Join(Statuses(), ", ")) 66 | } 67 | 68 | switch s := stmt.Status; s { 69 | case StatusNotAffected: 70 | // require a justification 71 | j := stmt.Justification 72 | is := stmt.ImpactStatement 73 | if j == "" && is == "" { 74 | return fmt.Errorf("either justification or impact statement must be defined when using status %q", s) 75 | } 76 | 77 | if j != "" && !j.Valid() { 78 | return fmt.Errorf("invalid justification value %q, must be one of [%s]", j, strings.Join(Justifications(), ", ")) 79 | } 80 | 81 | // irrelevant fields should not be set 82 | if v := stmt.ActionStatement; v != "" { 83 | return fmt.Errorf("action statement should not be set when using status %q (was set to %q)", s, v) 84 | } 85 | 86 | case StatusAffected: 87 | // irrelevant fields should not be set 88 | if v := stmt.Justification; v != "" { 89 | return fmt.Errorf("justification should not be set when using status %q (was set to %q)", s, v) 90 | } 91 | 92 | if v := stmt.ImpactStatement; v != "" { 93 | return fmt.Errorf("impact statement should not be set when using status %q (was set to %q)", s, v) 94 | } 95 | 96 | // action statement is now required 97 | if v := stmt.ActionStatement; v == "" { 98 | return fmt.Errorf("action statement must be set when using status %q", s) 99 | } 100 | 101 | case StatusUnderInvestigation: 102 | // irrelevant fields should not be set 103 | if v := stmt.Justification; v != "" { 104 | return fmt.Errorf("justification should not be set when using status %q (was set to %q)", s, v) 105 | } 106 | 107 | if v := stmt.ImpactStatement; v != "" { 108 | return fmt.Errorf("impact statement should not be set when using status %q (was set to %q)", s, v) 109 | } 110 | 111 | if v := stmt.ActionStatement; v != "" { 112 | return fmt.Errorf("action statement should not be set when using status %q (was set to %q)", s, v) 113 | } 114 | 115 | case StatusFixed: 116 | // irrelevant fields should not be set 117 | if v := stmt.Justification; v != "" { 118 | return fmt.Errorf("justification should not be set when using status %q (was set to %q)", s, v) 119 | } 120 | 121 | if v := stmt.ImpactStatement; v != "" { 122 | return fmt.Errorf("impact statement should not be set when using status %q (was set to %q)", s, v) 123 | } 124 | 125 | if v := stmt.ActionStatement; v != "" { 126 | return fmt.Errorf("action statement should not be set when using status %q (was set to %q)", s, v) 127 | } 128 | } 129 | 130 | return nil 131 | } 132 | 133 | // SortStatements does an "in-place" sort of the given slice of VEX statements. 134 | // 135 | // The documentTimestamp parameter is needed because statements without timestamps inherit the timestamp of the document. 136 | func SortStatements(stmts []Statement, documentTimestamp time.Time) { 137 | sort.SliceStable(stmts, func(i, j int) bool { 138 | vulnComparison := strings.Compare(stmts[i].Vulnerability, stmts[j].Vulnerability) 139 | if vulnComparison != 0 { 140 | // i.e. different vulnerabilities; sort by string comparison 141 | return vulnComparison < 0 142 | } 143 | 144 | // i.e. the same vulnerability; sort statements by timestamp 145 | 146 | iTime := stmts[i].Timestamp 147 | if iTime == nil || iTime.IsZero() { 148 | iTime = &documentTimestamp 149 | } 150 | 151 | jTime := stmts[j].Timestamp 152 | if jTime == nil || jTime.IsZero() { 153 | jTime = &documentTimestamp 154 | } 155 | 156 | if iTime == nil { 157 | return false 158 | } 159 | 160 | if jTime == nil { 161 | return true 162 | } 163 | 164 | return iTime.Before(*jTime) 165 | }) 166 | } 167 | -------------------------------------------------------------------------------- /internal/cmd/create.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package cmd 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "os" 12 | "strings" 13 | 14 | "github.com/spf13/cobra" 15 | 16 | "chainguard.dev/vex/pkg/vex" 17 | ) 18 | 19 | type createOptions struct { 20 | vexDocOptions 21 | vexStatementOptions 22 | outFilePath string 23 | } 24 | 25 | // Validates the options in context with arguments 26 | func (o *createOptions) Validate(args []string) error { 27 | if o.Status != string(vex.StatusAffected) && o.ActionStatement == vex.NoActionStatementMsg { 28 | o.ActionStatement = "" 29 | } 30 | if len(args) == 0 && len(o.Products) == 0 { 31 | return errors.New("a required product id is required to generate a valid VEX statement") 32 | } 33 | 34 | if len(args) < 2 && o.Vulnerability == "" { 35 | return errors.New("a vulnerability ID is required to generate a valid VEX statement") 36 | } 37 | 38 | if len(args) < 3 && o.Status == "" { 39 | return fmt.Errorf("a valid impact status is required, one of %s", strings.Join(vex.Statuses(), ", ")) 40 | } 41 | 42 | if len(args) >= 2 && o.Vulnerability != "" && args[1] != o.Vulnerability { 43 | return errors.New("vulnerability can only be specified once") 44 | } 45 | if len(args) >= 3 && o.Status != "" && args[2] != o.Status { 46 | return errors.New("status can only be specified once") 47 | } 48 | 49 | return nil 50 | } 51 | 52 | func addCreate(parentCmd *cobra.Command) { 53 | opts := createOptions{} 54 | createCmd := &cobra.Command{ 55 | Short: fmt.Sprintf("%s create: creates a new VEX document", appname), 56 | Long: fmt.Sprintf(`%s create: creates a new VEX document 57 | 58 | The create subcommand generates a single statement document 59 | from the command line. This is intended for simple use cases 60 | or to get a base document to get started. 61 | 62 | You can specify multiple products and customize the metadata of 63 | the document via the command line flags. %s will honor the 64 | SOURCE_DATE_EPOCH environment variable and use that date for 65 | the document (it can be formatted in UNIX time or RFC3339). 66 | 67 | If you don't specify an ID for the document, one will be generated 68 | using its canonicalization hash. 69 | 70 | Examples: 71 | 72 | # Generate a document stating that CVE-2023-12345 was fixed in the 73 | # git package of Wolfi: 74 | 75 | %s create "pkg:apk/wolfi/git@2.39.0-r1?arch=x86_64" CVE-2023-12345 fixed 76 | 77 | # You can specify more than one product. %s will read one from 78 | # the argument but you can control all parameters through command line 79 | # flags. Here's an example with two products in the same document: 80 | 81 | %s create --product="pkg:apk/wolfi/git@2.39.0-r1?arch=x86_64" \ 82 | --product="pkg:apk/wolfi/git@2.39.0-r1?arch=armv7" \ 83 | --vuln="CVE-2023-12345" \ 84 | --status="fixed" 85 | 86 | # not_affected statements need a justification: 87 | 88 | %s create --product="pkg:apk/wolfi/trivy@0.36.1-r0?arch=x86_64" \ 89 | --vuln="CVE-2023-12345" \ 90 | --status="not_affected" \ 91 | --justification="component_not_present" 92 | 93 | `, appname, appname, appname, appname, appname, appname), 94 | Use: "create [flags] [product_id [vuln_id [status]]]", 95 | Example: fmt.Sprintf("%s create \"pkg:apk/wolfi/git@2.39.0-r1?arch=x86_64\" CVE-2022-39260 fixed ", appname), 96 | SilenceUsage: false, 97 | SilenceErrors: false, 98 | PersistentPreRunE: initLogging, 99 | RunE: func(cmd *cobra.Command, args []string) error { 100 | if err := opts.Validate(args); err != nil { 101 | return err 102 | } 103 | // If we have arguments, add them 104 | for i := range args { 105 | switch i { 106 | case 0: 107 | opts.Products = append(opts.Products, args[i]) 108 | case 1: 109 | opts.Vulnerability = args[i] 110 | case 2: 111 | opts.Status = args[i] 112 | } 113 | } 114 | newDoc := vex.New() 115 | 116 | statement := vex.Statement{ 117 | Vulnerability: opts.Vulnerability, 118 | Products: opts.Products, 119 | Subcomponents: opts.Subcomponents, 120 | Status: vex.Status(opts.Status), 121 | StatusNotes: opts.StatusNotes, 122 | Justification: vex.Justification(opts.Justification), 123 | ImpactStatement: opts.ImpactStatement, 124 | ActionStatement: opts.ActionStatement, 125 | } 126 | 127 | if err := statement.Validate(); err != nil { 128 | return fmt.Errorf("invalid statement: %w", err) 129 | } 130 | 131 | newDoc.Statements = append(newDoc.Statements, statement) 132 | if _, err := newDoc.GenerateCanonicalID(); err != nil { 133 | return fmt.Errorf("generating document id: %w", err) 134 | } 135 | 136 | out := os.Stdout 137 | 138 | if opts.outFilePath != "" { 139 | f, err := os.Create(opts.outFilePath) 140 | if err != nil { 141 | return fmt.Errorf("opening VEX file to write document: %w", err) 142 | } 143 | out = f 144 | defer f.Close() 145 | } 146 | 147 | if err := newDoc.ToJSON(out); err != nil { 148 | return fmt.Errorf("writing new VEX document: %w", err) 149 | } 150 | 151 | if opts.outFilePath != "" { 152 | fmt.Fprintf(os.Stderr, " > VEX document written to %s\n", opts.outFilePath) 153 | } 154 | return nil 155 | }, 156 | } 157 | 158 | createCmd.PersistentFlags().StringVar( 159 | &opts.DocumentID, 160 | "id", 161 | "", 162 | "ID for the new VEX document (default will be computed)", 163 | ) 164 | 165 | createCmd.PersistentFlags().StringVar( 166 | &opts.Author, 167 | "author", 168 | vex.DefaultAuthor, 169 | "author to record in the new document", 170 | ) 171 | 172 | createCmd.PersistentFlags().StringVar( 173 | &opts.AuthorRole, 174 | "author-role", 175 | vex.DefaultRole, 176 | "author role to record in the new document", 177 | ) 178 | 179 | createCmd.PersistentFlags().StringVarP( 180 | &opts.Vulnerability, 181 | "vuln", 182 | "v", 183 | "", 184 | "vulnerability to add to the statement (eg CVE-2023-12345)", 185 | ) 186 | 187 | createCmd.PersistentFlags().StringSliceVarP( 188 | &opts.Products, 189 | "product", 190 | "p", 191 | []string{}, 192 | "list of products to list in the statement, at least one is required", 193 | ) 194 | 195 | createCmd.PersistentFlags().StringVarP( 196 | &opts.Status, 197 | "status", 198 | "s", 199 | "", 200 | fmt.Sprintf("status of the product vs the vulnerability, see '%s show statuses' for list", appname), 201 | ) 202 | 203 | createCmd.PersistentFlags().StringSliceVar( 204 | &opts.Products, 205 | "subcomponents", 206 | []string{}, 207 | "list of subcomponents to add to the statement", 208 | ) 209 | 210 | createCmd.PersistentFlags().StringVarP( 211 | &opts.Justification, 212 | "justification", 213 | "j", 214 | "", 215 | fmt.Sprintf("justification for not_affected status, see '%s show justifications' for list", appname), 216 | ) 217 | 218 | createCmd.PersistentFlags().StringVarP( 219 | &opts.ActionStatement, 220 | "action-statement", 221 | "a", 222 | vex.NoActionStatementMsg, 223 | "action statement for affected status", 224 | ) 225 | 226 | createCmd.PersistentFlags().StringVar( 227 | &opts.outFilePath, 228 | "file", 229 | "", 230 | "file to write the document (default is STDOUT)", 231 | ) 232 | 233 | parentCmd.AddCommand(createCmd) 234 | } 235 | -------------------------------------------------------------------------------- /pkg/vex/vex.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package vex 7 | 8 | import ( 9 | "crypto/sha256" 10 | "encoding/json" 11 | "errors" 12 | "fmt" 13 | "io" 14 | "os" 15 | "sort" 16 | "strconv" 17 | "strings" 18 | "time" 19 | 20 | "github.com/sirupsen/logrus" 21 | "gopkg.in/yaml.v3" 22 | 23 | "chainguard.dev/vex/pkg/csaf" 24 | ) 25 | 26 | const ( 27 | // TypeURI is the type used to describe VEX documents, e.g. within [in-toto 28 | // statements]. 29 | // 30 | // [in-toto statements]: https://github.com/in-toto/attestation/blob/main/spec/README.md#statement 31 | TypeURI = "text/vex" 32 | 33 | // DefaultAuthor is the default value for a document's Author field. 34 | DefaultAuthor = "Unknown Author" 35 | 36 | // DefaultRole is the default value for a document's AuthorRole field. 37 | DefaultRole = "Document Creator" 38 | 39 | // Context is the URL of the json-ld context definition 40 | Context = "https://openvex.dev/ns" 41 | 42 | // PublicNamespace is the public openvex namespace for common @ids 43 | PublicNamespace = "https://openvex.dev/docs" 44 | 45 | // NoActionStatementMsg is the action statement that informs that there is no action statement :/ 46 | NoActionStatementMsg = "No action statement provided" 47 | ) 48 | 49 | // The VEX type represents a VEX document and all of its contained information. 50 | type VEX struct { 51 | Metadata 52 | Statements []Statement `json:"statements"` 53 | } 54 | 55 | // The Metadata type represents the metadata associated with a VEX document. 56 | type Metadata struct { 57 | // Context is the URL pointing to the jsonld context definition 58 | Context string `json:"@context"` 59 | 60 | // ID is the identifying string for the VEX document. This should be unique per 61 | // document. 62 | ID string `json:"@id"` 63 | 64 | // Author is the identifier for the author of the VEX statement, ideally a common 65 | // name, may be a URI. [author] is an individual or organization. [author] 66 | // identity SHOULD be cryptographically associated with the signature of the VEX 67 | // statement or document or transport. 68 | Author string `json:"author"` 69 | 70 | // AuthorRole describes the role of the document Author. 71 | AuthorRole string `json:"role"` 72 | 73 | // Timestamp defines the time at which the document was issued. 74 | Timestamp *time.Time `json:"timestamp"` 75 | 76 | // Version is the document version. It must be incremented when any content 77 | // within the VEX document changes, including any VEX statements included within 78 | // the VEX document. 79 | Version string `json:"version"` 80 | 81 | // Tooling expresses how the VEX document and contained VEX statements were 82 | // generated. It's optional. It may specify tools or automated processes used in 83 | // the document or statement generation. 84 | Tooling string `json:"tooling,omitempty"` 85 | 86 | // Supplier is an optional field. 87 | Supplier string `json:"supplier,omitempty"` 88 | } 89 | 90 | // New returns a new, initialized VEX document. 91 | func New() VEX { 92 | now := time.Now() 93 | t, err := DateFromEnv() 94 | if err != nil { 95 | logrus.Warn(err) 96 | } 97 | if t != nil { 98 | now = *t 99 | } 100 | return VEX{ 101 | Metadata: Metadata{ 102 | Context: Context, 103 | Author: DefaultAuthor, 104 | AuthorRole: DefaultRole, 105 | Version: "1", 106 | Timestamp: &now, 107 | }, 108 | Statements: []Statement{}, 109 | } 110 | } 111 | 112 | // Load reads the VEX document file at the given path and returns a decoded VEX 113 | // object. If Load is unable to read the file or decode the document, it returns 114 | // an error. 115 | func Load(path string) (*VEX, error) { 116 | data, err := os.ReadFile(path) 117 | if err != nil { 118 | return nil, fmt.Errorf("loading VEX file: %w", err) 119 | } 120 | 121 | vexDoc := &VEX{} 122 | if err := json.Unmarshal(data, vexDoc); err != nil { 123 | return nil, fmt.Errorf("unmarshaling VEX document: %w", err) 124 | } 125 | return vexDoc, nil 126 | } 127 | 128 | // OpenYAML opens a VEX file in YAML format. 129 | func OpenYAML(path string) (*VEX, error) { 130 | data, err := os.ReadFile(path) 131 | if err != nil { 132 | return nil, fmt.Errorf("opening YAML file: %w", err) 133 | } 134 | vexDoc := New() 135 | if err := yaml.Unmarshal(data, &vexDoc); err != nil { 136 | return nil, fmt.Errorf("unmarshalling VEX data: %w", err) 137 | } 138 | return &vexDoc, nil 139 | } 140 | 141 | // OpenJSON opens a VEX file in JSON format. 142 | func OpenJSON(path string) (*VEX, error) { 143 | data, err := os.ReadFile(path) 144 | if err != nil { 145 | return nil, fmt.Errorf("opening JSON file: %w", err) 146 | } 147 | vexDoc := New() 148 | if err := json.Unmarshal(data, &vexDoc); err != nil { 149 | return nil, fmt.Errorf("unmarshalling VEX data: %w", err) 150 | } 151 | return &vexDoc, nil 152 | } 153 | 154 | // ToJSON serializes the VEX document to JSON and writes it to the passed writer. 155 | func (vexDoc *VEX) ToJSON(w io.Writer) error { 156 | enc := json.NewEncoder(w) 157 | enc.SetIndent("", " ") 158 | enc.SetEscapeHTML(false) 159 | 160 | if err := enc.Encode(vexDoc); err != nil { 161 | return fmt.Errorf("encoding vex document: %w", err) 162 | } 163 | return nil 164 | } 165 | 166 | // StatementFromID returns a statement for a given vulnerability if there is one. 167 | func (vexDoc *VEX) StatementFromID(id string) *Statement { 168 | for _, statement := range vexDoc.Statements { //nolint:gocritic // turning off for rule rangeValCopy 169 | if statement.Vulnerability == id { 170 | logrus.Infof("VEX doc contains statement for CVE %s", id) 171 | return &statement 172 | } 173 | } 174 | return nil 175 | } 176 | 177 | // SortDocuments sorts and returns a slice of documents based on their date. 178 | // VEXes should be applied sequentially in chronological order as they capture 179 | // knowledge about an artifact as it changes over time. 180 | func SortDocuments(docs []*VEX) []*VEX { 181 | sort.Slice(docs, func(i, j int) bool { 182 | if docs[j].Timestamp == nil { 183 | return true 184 | } 185 | if docs[i].Timestamp == nil { 186 | return false 187 | } 188 | return docs[i].Timestamp.Before(*(docs[j].Timestamp)) 189 | }) 190 | return docs 191 | } 192 | 193 | // OpenCSAF opens a CSAF document and builds a VEX object from it. 194 | func OpenCSAF(path string, products []string) (*VEX, error) { 195 | csafDoc, err := csaf.Open(path) 196 | if err != nil { 197 | return nil, fmt.Errorf("opening csaf doc: %w", err) 198 | } 199 | 200 | productDict := map[string]string{} 201 | for _, pid := range products { 202 | productDict[pid] = pid 203 | } 204 | 205 | // If no products were specified, we use the first one 206 | if len(products) == 0 { 207 | p := csafDoc.FirstProductName() 208 | if p == "" { 209 | // Error? I think so. 210 | return nil, errors.New("unable to find a product ID in CSAF document") 211 | } 212 | productDict[p] = p 213 | } 214 | 215 | // Create the vex doc 216 | v := &VEX{ 217 | Metadata: Metadata{ 218 | ID: csafDoc.Document.Tracking.ID, 219 | Author: "", 220 | AuthorRole: "", 221 | Timestamp: &time.Time{}, 222 | }, 223 | Statements: []Statement{}, 224 | } 225 | 226 | // Cycle the CSAF vulns list and get those that apply 227 | for _, c := range csafDoc.Vulnerabilities { 228 | for status, docProducts := range c.ProductStatus { 229 | for _, productID := range docProducts { 230 | if _, ok := productDict[productID]; ok { 231 | // Check we have a valid status 232 | if StatusFromCSAF(status) == "" { 233 | return nil, fmt.Errorf("invalid status for product %s", productID) 234 | } 235 | 236 | // TODO search the threats struct for justification, etc 237 | just := "" 238 | for _, t := range c.Threats { 239 | // Search the threats for a justification 240 | for _, p := range t.ProductIDs { 241 | if p == productID { 242 | just = t.Details 243 | } 244 | } 245 | } 246 | 247 | v.Statements = append(v.Statements, Statement{ 248 | Vulnerability: c.CVE, 249 | Status: StatusFromCSAF(status), 250 | Justification: "", // Justifications are not machine readable in csaf, it seems 251 | ActionStatement: just, 252 | Products: products, 253 | }) 254 | } 255 | } 256 | } 257 | } 258 | 259 | return v, nil 260 | } 261 | 262 | // CanonicalHash returns a hash representing the state of impact statements 263 | // expressed in it. This hash should be constant as long as the impact 264 | // statements are not modified. Changes in extra information and metadata 265 | // will not alter the hash. 266 | func (vexDoc *VEX) CanonicalHash() (string, error) { 267 | // Here's the algo: 268 | 269 | // 1. Start with the document date. In unixtime to avoid format variance. 270 | cString := fmt.Sprintf("%d", vexDoc.Timestamp.Unix()) 271 | 272 | // 2. Document version 273 | cString += fmt.Sprintf(":%s", vexDoc.Version) 274 | 275 | // 3. Sort the statements 276 | stmts := vexDoc.Statements 277 | SortStatements(stmts, *vexDoc.Timestamp) 278 | 279 | // 4. Now add the data from each statement 280 | //nolint:gocritic 281 | for _, s := range stmts { 282 | // 4a. Vulnerability 283 | cString += fmt.Sprintf(":%s", s.Vulnerability) 284 | // 4b. Status + Justification 285 | cString += fmt.Sprintf(":%s:%s", s.Status, s.Justification) 286 | // 4c. Statement time, in unixtime. If it exists, if not the doc's 287 | if s.Timestamp != nil { 288 | cString += fmt.Sprintf(":%d", s.Timestamp.Unix()) 289 | } else { 290 | cString += fmt.Sprintf(":%d", vexDoc.Timestamp.Unix()) 291 | } 292 | // 4d. Sorted products 293 | prods := s.Products 294 | sort.Strings(prods) 295 | cString += fmt.Sprintf(":%s", strings.Join(prods, ":")) 296 | } 297 | 298 | // 5. Hash the string in sha256 and return 299 | h := sha256.New() 300 | if _, err := h.Write([]byte(cString)); err != nil { 301 | return "", fmt.Errorf("hashing canonicalization string: %w", err) 302 | } 303 | return fmt.Sprintf("%x", h.Sum(nil)), nil 304 | } 305 | 306 | // GenerateCanonicalID generates an ID for the document. The ID will be 307 | // based on the canonicalization hash. This means that documents 308 | // with the same impact statements will always get the same ID. 309 | // Trying to generate the id of a doc with an existing ID will 310 | // not do anything. 311 | func (vexDoc *VEX) GenerateCanonicalID() (string, error) { 312 | if vexDoc.ID != "" { 313 | return vexDoc.ID, nil 314 | } 315 | cHash, err := vexDoc.CanonicalHash() 316 | if err != nil { 317 | return "", fmt.Errorf("getting canonical hash: %w", err) 318 | } 319 | 320 | // For common namespaced documents we namespace them into /public 321 | vexDoc.ID = fmt.Sprintf("%s/public/vex-%s", PublicNamespace, cHash) 322 | return vexDoc.ID, nil 323 | } 324 | 325 | func DateFromEnv() (*time.Time, error) { 326 | // Support envvar for reproducible vexing 327 | d := os.Getenv("SOURCE_DATE_EPOCH") 328 | if d == "" { 329 | return nil, nil 330 | } 331 | 332 | var t time.Time 333 | sec, err := strconv.ParseInt(d, 10, 64) 334 | if err == nil { 335 | t = time.Unix(sec, 0) 336 | } else { 337 | t, err = time.Parse(time.RFC3339, d) 338 | if err != nil { 339 | return nil, fmt.Errorf("failed to parse envvar SOURCE_DATE_EPOCH: %w", err) 340 | } 341 | } 342 | return &t, nil 343 | } 344 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /pkg/ctl/implementation.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022 Chainguard, Inc. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | package ctl 7 | 8 | import ( 9 | "bytes" 10 | "context" 11 | "crypto/sha256" 12 | "encoding/base64" 13 | "encoding/json" 14 | "errors" 15 | "fmt" 16 | "regexp" 17 | "sort" 18 | "strings" 19 | "time" 20 | 21 | "github.com/google/go-containerregistry/pkg/name" 22 | gosarif "github.com/owenrumney/go-sarif/sarif" 23 | ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse" 24 | "github.com/sigstore/cosign/cmd/cosign/cli/options" 25 | "github.com/sigstore/cosign/pkg/cosign" 26 | "github.com/sigstore/cosign/pkg/oci/mutate" 27 | ociremote "github.com/sigstore/cosign/pkg/oci/remote" 28 | "github.com/sigstore/cosign/pkg/oci/static" 29 | "github.com/sigstore/cosign/pkg/types" 30 | "github.com/sirupsen/logrus" 31 | "sigs.k8s.io/release-utils/util" 32 | 33 | "chainguard.dev/vex/pkg/attestation" 34 | "chainguard.dev/vex/pkg/sarif" 35 | "chainguard.dev/vex/pkg/vex" 36 | ) 37 | 38 | const IntotoPayloadType = "application/vnd.in-toto+json" 39 | 40 | type Implementation interface { 41 | ApplySingleVEX(*sarif.Report, *vex.VEX) (*sarif.Report, error) 42 | SortDocuments([]*vex.VEX) []*vex.VEX 43 | OpenVexData(Options, []string) ([]*vex.VEX, error) 44 | Sort(docs []*vex.VEX) []*vex.VEX 45 | AttestationBytes(*attestation.Attestation) ([]byte, error) 46 | Attach(context.Context, *attestation.Attestation, string) error 47 | SourceType(uri string) (string, error) 48 | ReadImageAttestations(context.Context, Options, string) ([]*vex.VEX, error) 49 | Merge(context.Context, *MergeOptions, []*vex.VEX) (*vex.VEX, error) 50 | LoadFiles(context.Context, []string) ([]*vex.VEX, error) 51 | } 52 | 53 | type defaultVexCtlImplementation struct{} 54 | 55 | var cveRegexp regexp.Regexp 56 | 57 | func init() { 58 | cveRegexp = *regexp.MustCompile(`^(CVE-\d+-\d+)`) 59 | } 60 | 61 | func (impl *defaultVexCtlImplementation) SortDocuments(docs []*vex.VEX) []*vex.VEX { 62 | return vex.SortDocuments(docs) 63 | } 64 | 65 | func (impl *defaultVexCtlImplementation) ApplySingleVEX(report *sarif.Report, vexDoc *vex.VEX) (*sarif.Report, error) { 66 | newReport := *report 67 | logrus.Infof("VEX document contains %d statements", len(vexDoc.Statements)) 68 | logrus.Infof("+%v Runs: %d\n", report, len(report.Runs)) 69 | // Search for negative VEX statements, that is those that cancel a CVE 70 | for i := range report.Runs { 71 | newResults := []*gosarif.Result{} 72 | logrus.Infof("Inspecting run #%d containing %d results", i, len(report.Runs[i].Results)) 73 | for _, res := range report.Runs[i].Results { 74 | // Normalize the CVE IDs 75 | m := cveRegexp.FindStringSubmatch(*res.RuleID) 76 | if len(m) != 2 { 77 | logrus.Errorf( 78 | "Invalid rulename in sarif report, expected CVE identifier, got %s", 79 | *res.RuleID, 80 | ) 81 | newResults = append(newResults, res) 82 | continue 83 | } 84 | id := m[1] 85 | // TODO: Trim rule ID to CVE as Grype adds junk to the CVE ID 86 | statement := vexDoc.StatementFromID(id) 87 | logrus.Infof("Checking %s", id) 88 | if statement != nil { 89 | logrus.Infof("Statement is for %s and status is %s", statement.Vulnerability, statement.Status) 90 | if statement.Status == vex.StatusNotAffected || 91 | statement.Status == vex.StatusFixed { 92 | logrus.Infof("Found VEX Statement for %s: %s", id, statement.Status) 93 | continue 94 | } 95 | } 96 | newResults = append(newResults, res) 97 | } 98 | newReport.Runs[i].Results = newResults 99 | } 100 | return &newReport, nil 101 | } 102 | 103 | // OpenVexData returns a set of vex documents from the paths received 104 | func (impl *defaultVexCtlImplementation) OpenVexData(opts Options, paths []string) ([]*vex.VEX, error) { 105 | vexes := []*vex.VEX{} 106 | for _, path := range paths { 107 | var v *vex.VEX 108 | var err error 109 | switch opts.Format { 110 | case "vex", "json", "": 111 | v, err = vex.OpenJSON(path) 112 | case "yaml": 113 | v, err = vex.OpenYAML(path) 114 | case "csaf": 115 | v, err = vex.OpenCSAF(path, opts.Products) 116 | } 117 | if err != nil { 118 | return nil, fmt.Errorf("opening document: %w", err) 119 | } 120 | vexes = append(vexes, v) 121 | } 122 | return vexes, nil 123 | } 124 | 125 | func (impl *defaultVexCtlImplementation) Sort(docs []*vex.VEX) []*vex.VEX { 126 | return vex.SortDocuments(docs) 127 | } 128 | 129 | func (impl *defaultVexCtlImplementation) AttestationBytes(att *attestation.Attestation) ([]byte, error) { 130 | var b bytes.Buffer 131 | if err := att.ToJSON(&b); err != nil { 132 | return nil, fmt.Errorf("serializing attestation to json: %w", err) 133 | } 134 | return b.Bytes(), nil 135 | } 136 | 137 | func (impl *defaultVexCtlImplementation) Attach(ctx context.Context, att *attestation.Attestation, imageRef string) error { 138 | env := ssldsse.Envelope{} 139 | regOpts := options.RegistryOptions{} 140 | remoteOpts, err := regOpts.ClientOpts(ctx) 141 | if err != nil { 142 | return fmt.Errorf("getting OCI remote options: %w", err) 143 | } 144 | 145 | var b bytes.Buffer 146 | if err := att.ToJSON(&b); err != nil { 147 | return fmt.Errorf("getting attestation JSON") 148 | } 149 | decoder := json.NewDecoder(&b) 150 | for decoder.More() { 151 | if err := decoder.Decode(&env); err != nil { 152 | return err 153 | } 154 | 155 | payload, err := json.Marshal(env) 156 | if err != nil { 157 | return err 158 | } 159 | 160 | if env.PayloadType != IntotoPayloadType { 161 | return fmt.Errorf("invalid payloadType %s on envelope. Expected %s", env.PayloadType, types.IntotoPayloadType) 162 | } 163 | 164 | ref, err := name.ParseReference(imageRef) 165 | if err != nil { 166 | return err 167 | } 168 | digest, err := ociremote.ResolveDigest(ref, remoteOpts...) 169 | if err != nil { 170 | return err 171 | } 172 | // Overwrite "ref" with a digest to avoid a race where we use a tag 173 | // multiple times, and it potentially points to different things at 174 | // each access. 175 | ref = digest //nolint:ineffassign 176 | 177 | opts := []static.Option{static.WithLayerMediaType(types.DssePayloadType)} 178 | att, err := static.NewAttestation(payload, opts...) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | se, err := ociremote.SignedEntity(digest, remoteOpts...) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | newSE, err := mutate.AttachAttestationToEntity(se, att) 189 | if err != nil { 190 | return err 191 | } 192 | 193 | // Publish the signatures associated with this entity 194 | err = ociremote.WriteAttestations(digest.Repository, newSE, remoteOpts...) 195 | if err != nil { 196 | return err 197 | } 198 | } 199 | 200 | return nil 201 | } 202 | 203 | // SourceType returns a string indicating what kind of vex 204 | // source a URI points to 205 | func (impl *defaultVexCtlImplementation) SourceType(uri string) (string, error) { 206 | if util.Exists(uri) { 207 | return "file", nil 208 | } 209 | 210 | _, err := name.ParseReference(uri) 211 | if err == nil { 212 | return "image", nil 213 | } 214 | 215 | return "", errors.New("unable to resolve the vex source location") 216 | } 217 | 218 | // DownloadAttestation 219 | func (impl *defaultVexCtlImplementation) ReadImageAttestations( 220 | ctx context.Context, opts Options, refString string, 221 | ) (vexes []*vex.VEX, err error) { 222 | // Parsae the image reference 223 | ref, err := name.ParseReference(refString) 224 | if err != nil { 225 | return nil, fmt.Errorf("parsing image reference: %w", err) 226 | } 227 | regOpts := options.RegistryOptions{} 228 | remoteOpts, err := regOpts.ClientOpts(ctx) 229 | if err != nil { 230 | return nil, fmt.Errorf("getting OCI remote options: %w", err) 231 | } 232 | payloads, err := cosign.FetchAttestationsForReference(ctx, ref, remoteOpts...) 233 | if err != nil { 234 | return nil, fmt.Errorf("fetching attached attestation: %w", err) 235 | } 236 | vexes = []*vex.VEX{} 237 | for _, dssePayload := range payloads { 238 | vexData, err := impl.ReadSignedVEX(dssePayload) 239 | if err != nil { 240 | return nil, fmt.Errorf("opening dsse payload: %w", err) 241 | } 242 | vexes = append(vexes, vexData) 243 | } 244 | return vexes, nil 245 | } 246 | 247 | // ReadSignedVEX returns the vex data inside a signed envelope 248 | func (impl *defaultVexCtlImplementation) ReadSignedVEX(dssePayload cosign.AttestationPayload) (*vex.VEX, error) { 249 | if dssePayload.PayloadType != IntotoPayloadType { 250 | logrus.Info("Signed envelope does not contain an in-toto attestation") 251 | return nil, nil 252 | } 253 | 254 | data, err := base64.StdEncoding.DecodeString(dssePayload.PayLoad) 255 | if err != nil { 256 | return nil, fmt.Errorf("decoding signed attestation: %w", err) 257 | } 258 | fmt.Printf("%s\n", string(data)) 259 | 260 | // Unmarshall the attestation 261 | att := &attestation.Attestation{} 262 | if err := json.Unmarshal(data, att); err != nil { 263 | return nil, fmt.Errorf("unmarshalling attestation JSON: %w", err) 264 | } 265 | 266 | if att.PredicateType != vex.TypeURI { 267 | return nil, nil 268 | } 269 | 270 | return &att.Predicate, nil 271 | } 272 | 273 | type MergeOptions struct { 274 | DocumentID string // ID to use in the new document 275 | Author string // Author to use in the new document 276 | AuthorRole string // Role of the document author 277 | Products []string // Product IDs to consider 278 | Vulnerabilities []string // IDs of vulnerabilities to merge 279 | } 280 | 281 | // Merge combines the statements from a number of documents into 282 | // a new one, preserving time context from each of them. 283 | func (impl *defaultVexCtlImplementation) Merge( 284 | _ context.Context, mergeOpts *MergeOptions, docs []*vex.VEX, 285 | ) (*vex.VEX, error) { 286 | if len(docs) == 0 { 287 | return nil, fmt.Errorf("at least one vex document is required to merge") 288 | } 289 | 290 | docID := mergeOpts.DocumentID 291 | // If no document id is specified we compute a 292 | // deterministic ID using the merged docs 293 | if docID == "" { 294 | ids := []string{} 295 | for i, d := range docs { 296 | if d.ID == "" { 297 | ids = append(ids, fmt.Sprintf("VEX-DOC-%d", i)) 298 | } else { 299 | ids = append(ids, d.ID) 300 | } 301 | } 302 | 303 | sort.Strings(ids) 304 | h := sha256.New() 305 | h.Write([]byte(strings.Join(ids, ":"))) 306 | // Hash the sorted IDs list 307 | docID = fmt.Sprintf("merged-vex-%x", h.Sum(nil)) 308 | } 309 | now := time.Now() 310 | newDoc := &vex.VEX{ 311 | Metadata: vex.Metadata{ 312 | ID: docID, // TODO 313 | Author: mergeOpts.Author, 314 | AuthorRole: mergeOpts.AuthorRole, 315 | Timestamp: &now, 316 | }, 317 | } 318 | 319 | t, err := vex.DateFromEnv() 320 | if err != nil { 321 | return nil, fmt.Errorf("reading date from env: %w", err) 322 | } 323 | 324 | if t != nil { 325 | newDoc.Metadata.Timestamp = t 326 | } 327 | 328 | ss := []vex.Statement{} 329 | 330 | iProds := map[string]struct{}{} 331 | iVulns := map[string]struct{}{} 332 | for _, id := range mergeOpts.Products { 333 | iProds[id] = struct{}{} 334 | } 335 | for _, id := range mergeOpts.Vulnerabilities { 336 | iVulns[id] = struct{}{} 337 | } 338 | 339 | for _, doc := range docs { 340 | LOOP_STATEMENTS: 341 | for _, s := range doc.Statements { //nolint:gocritic // this IS supposed to copy 342 | if len(iProds) > 0 { 343 | for _, pid := range s.Products { 344 | if _, ok := iProds[pid]; !ok { 345 | continue LOOP_STATEMENTS 346 | } 347 | } 348 | } 349 | 350 | if len(iVulns) > 0 { 351 | if _, ok := iProds[s.Vulnerability]; !ok { 352 | continue LOOP_STATEMENTS 353 | } 354 | } 355 | 356 | // If statement does not have a timestamp, cascade 357 | // the timestamp down from the document. 358 | // See https://github.com/chainguard-dev/vex/issues/49 359 | if s.Timestamp == nil { 360 | if doc.Timestamp == nil { 361 | return nil, errors.New("unable to cascade timestamp from doc to timeless statement") 362 | } 363 | s.Timestamp = doc.Timestamp 364 | } 365 | 366 | ss = append(ss, s) 367 | } 368 | } 369 | 370 | vex.SortStatements(ss, *newDoc.Metadata.Timestamp) 371 | 372 | newDoc.Statements = ss 373 | 374 | return newDoc, nil 375 | } 376 | 377 | // LoadFiles loads multiple vex files from disk 378 | func (impl *defaultVexCtlImplementation) LoadFiles( 379 | _ context.Context, filePaths []string, 380 | ) ([]*vex.VEX, error) { 381 | vexes := make([]*vex.VEX, len(filePaths)) 382 | for i, path := range filePaths { 383 | doc, err := vex.Load(path) 384 | if err != nil { 385 | return nil, fmt.Errorf("error loading file: %w", err) 386 | } 387 | vexes[i] = doc 388 | } 389 | return vexes, nil 390 | } 391 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module chainguard.dev/vex 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/google/go-containerregistry v0.12.1 7 | github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add 8 | github.com/owenrumney/go-sarif v1.1.1 9 | github.com/secure-systems-lab/go-securesystemslib v0.4.0 10 | github.com/sigstore/cosign v1.13.1 11 | github.com/sigstore/sigstore v1.5.1 12 | github.com/sirupsen/logrus v1.9.0 13 | github.com/spf13/cobra v1.6.1 14 | gopkg.in/yaml.v3 v3.0.1 15 | sigs.k8s.io/release-utils v0.7.3 16 | ) 17 | 18 | require ( 19 | bitbucket.org/creachadair/shell v0.0.7 // indirect 20 | cloud.google.com/go/compute v1.14.0 // indirect 21 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 22 | github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0 // indirect 23 | github.com/Azure/azure-sdk-for-go v67.3.0+incompatible // indirect 24 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 25 | github.com/Azure/go-autorest/autorest v0.11.28 // indirect 26 | github.com/Azure/go-autorest/autorest/adal v0.9.21 // indirect 27 | github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect 28 | github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect 29 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 30 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 31 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 32 | github.com/Microsoft/go-winio v0.6.0 // indirect 33 | github.com/ThalesIgnite/crypto11 v1.2.5 // indirect 34 | github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect 35 | github.com/alibabacloud-go/cr-20160607 v1.0.1 // indirect 36 | github.com/alibabacloud-go/cr-20181201 v1.0.10 // indirect 37 | github.com/alibabacloud-go/darabonba-openapi v0.1.18 // indirect 38 | github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect 39 | github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect 40 | github.com/alibabacloud-go/openapi-util v0.0.11 // indirect 41 | github.com/alibabacloud-go/tea v1.1.18 // indirect 42 | github.com/alibabacloud-go/tea-utils v1.4.4 // indirect 43 | github.com/alibabacloud-go/tea-xml v1.1.2 // indirect 44 | github.com/aliyun/credentials-go v1.2.3 // indirect 45 | github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect 46 | github.com/aws/aws-sdk-go-v2 v1.17.3 // indirect 47 | github.com/aws/aws-sdk-go-v2/config v1.18.8 // indirect 48 | github.com/aws/aws-sdk-go-v2/credentials v1.13.8 // indirect 49 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 // indirect 50 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 // indirect 51 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 // indirect 52 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 // indirect 53 | github.com/aws/aws-sdk-go-v2/service/ecr v1.15.0 // indirect 54 | github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.12.0 // indirect 55 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 // indirect 56 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.0 // indirect 57 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.0 // indirect 58 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.0 // indirect 59 | github.com/aws/smithy-go v1.13.5 // indirect 60 | github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220228164355-396b2034c795 // indirect 61 | github.com/benbjohnson/clock v1.1.0 // indirect 62 | github.com/beorn7/perks v1.0.1 // indirect 63 | github.com/bgentry/speakeasy v0.1.0 // indirect 64 | github.com/blang/semver v3.5.1+incompatible // indirect 65 | github.com/blang/semver/v4 v4.0.0 // indirect 66 | github.com/cenkalti/backoff/v4 v4.1.3 // indirect 67 | github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect 68 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 69 | github.com/chrismellard/docker-credential-acr-env v0.0.0-20220119192733-fe33c00cee21 // indirect 70 | github.com/clbanning/mxj/v2 v2.5.6 // indirect 71 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect 72 | github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect 73 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect 74 | github.com/containerd/stargz-snapshotter/estargz v0.12.1 // indirect 75 | github.com/coreos/go-oidc/v3 v3.5.0 // indirect 76 | github.com/coreos/go-semver v0.3.0 // indirect 77 | github.com/coreos/go-systemd/v22 v22.3.2 // indirect 78 | github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect 79 | github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b // indirect 80 | github.com/davecgh/go-spew v1.1.1 // indirect 81 | github.com/dimchansky/utfbom v1.1.1 // indirect 82 | github.com/docker/cli v20.10.20+incompatible // indirect 83 | github.com/docker/distribution v2.8.1+incompatible // indirect 84 | github.com/docker/docker v20.10.20+incompatible // indirect 85 | github.com/docker/docker-credential-helpers v0.7.0 // indirect 86 | github.com/dustin/go-humanize v1.0.0 // indirect 87 | github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect 88 | github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect 89 | github.com/fsnotify/fsnotify v1.5.4 // indirect 90 | github.com/fullstorydev/grpcurl v1.8.7 // indirect 91 | github.com/go-chi/chi v4.1.2+incompatible // indirect 92 | github.com/go-jose/go-jose/v3 v3.0.0 // indirect 93 | github.com/go-logr/logr v1.2.3 // indirect 94 | github.com/go-logr/stdr v1.2.2 // indirect 95 | github.com/go-openapi/analysis v0.21.4 // indirect 96 | github.com/go-openapi/errors v0.20.3 // indirect 97 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 98 | github.com/go-openapi/jsonreference v0.20.0 // indirect 99 | github.com/go-openapi/loads v0.21.2 // indirect 100 | github.com/go-openapi/runtime v0.24.2 // indirect 101 | github.com/go-openapi/spec v0.20.7 // indirect 102 | github.com/go-openapi/strfmt v0.21.3 // indirect 103 | github.com/go-openapi/swag v0.22.3 // indirect 104 | github.com/go-openapi/validate v0.22.0 // indirect 105 | github.com/go-piv/piv-go v1.10.0 // indirect 106 | github.com/go-playground/locales v0.14.0 // indirect 107 | github.com/go-playground/universal-translator v0.18.0 // indirect 108 | github.com/go-playground/validator/v10 v10.11.0 // indirect 109 | github.com/gogo/protobuf v1.3.2 // indirect 110 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 111 | github.com/golang-jwt/jwt/v4 v4.4.2 // indirect 112 | github.com/golang/glog v1.0.0 // indirect 113 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 114 | github.com/golang/mock v1.6.0 // indirect 115 | github.com/golang/protobuf v1.5.2 // indirect 116 | github.com/golang/snappy v0.0.4 // indirect 117 | github.com/google/btree v1.1.2 // indirect 118 | github.com/google/certificate-transparency-go v1.1.3 // indirect 119 | github.com/google/go-cmp v0.5.9 // indirect 120 | github.com/google/go-github/v45 v45.2.0 // indirect 121 | github.com/google/go-querystring v1.1.0 // indirect 122 | github.com/google/gofuzz v1.2.0 // indirect 123 | github.com/google/trillian v1.5.0 // indirect 124 | github.com/googleapis/enterprise-certificate-proxy v0.2.1 // indirect 125 | github.com/googleapis/gnostic v0.5.5 // indirect 126 | github.com/gorilla/websocket v1.4.2 // indirect 127 | github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect 128 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 129 | github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect 130 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3 // indirect 131 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 132 | github.com/hashicorp/go-retryablehttp v0.7.1 // indirect 133 | github.com/hashicorp/hcl v1.0.0 // indirect 134 | github.com/imdario/mergo v0.3.12 // indirect 135 | github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect 136 | github.com/jhump/protoreflect v1.14.0 // indirect 137 | github.com/jmespath/go-jmespath v0.4.0 // indirect 138 | github.com/jonboulle/clockwork v0.3.0 // indirect 139 | github.com/josharian/intern v1.0.0 // indirect 140 | github.com/json-iterator/go v1.1.12 // indirect 141 | github.com/klauspost/compress v1.15.11 // indirect 142 | github.com/leodido/go-urn v1.2.1 // indirect 143 | github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf // indirect 144 | github.com/magiconair/properties v1.8.6 // indirect 145 | github.com/mailru/easyjson v0.7.7 // indirect 146 | github.com/mattn/go-runewidth v0.0.13 // indirect 147 | github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect 148 | github.com/miekg/pkcs11 v1.1.1 // indirect 149 | github.com/mitchellh/go-homedir v1.1.0 // indirect 150 | github.com/mitchellh/mapstructure v1.5.0 // indirect 151 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 152 | github.com/modern-go/reflect2 v1.0.2 // indirect 153 | github.com/mozillazg/docker-credential-acr-helper v0.3.0 // indirect 154 | github.com/oklog/ulid v1.3.1 // indirect 155 | github.com/olekukonko/tablewriter v0.0.5 // indirect 156 | github.com/opencontainers/go-digest v1.0.0 // indirect 157 | github.com/opencontainers/image-spec v1.1.0-rc2 // indirect 158 | github.com/opentracing/opentracing-go v1.2.0 // indirect 159 | github.com/pelletier/go-toml v1.9.5 // indirect 160 | github.com/pelletier/go-toml/v2 v2.0.5 // indirect 161 | github.com/pkg/errors v0.9.1 // indirect 162 | github.com/pmezard/go-difflib v1.0.0 // indirect 163 | github.com/prometheus/client_golang v1.13.0 // indirect 164 | github.com/prometheus/client_model v0.3.0 // indirect 165 | github.com/prometheus/common v0.37.0 // indirect 166 | github.com/prometheus/procfs v0.8.0 // indirect 167 | github.com/rivo/uniseg v0.2.0 // indirect 168 | github.com/rogpeppe/go-internal v1.9.0 // indirect 169 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 170 | github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 // indirect 171 | github.com/segmentio/ksuid v1.0.4 // indirect 172 | github.com/sigstore/fulcio v0.6.0 // indirect 173 | github.com/sigstore/rekor v0.12.1-0.20220915152154-4bb6f441c1b2 // indirect 174 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect 175 | github.com/soheilhy/cmux v0.1.5 // indirect 176 | github.com/spf13/afero v1.8.2 // indirect 177 | github.com/spf13/cast v1.5.0 // indirect 178 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 179 | github.com/spf13/viper v1.13.0 // indirect 180 | github.com/spiffe/go-spiffe/v2 v2.1.1 // indirect 181 | github.com/subosito/gotenv v1.4.1 // indirect 182 | github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect 183 | github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 // indirect 184 | github.com/thales-e-security/pool v0.0.2 // indirect 185 | github.com/theupdateframework/go-tuf v0.5.2-0.20220930112810-3890c1e7ace4 // indirect 186 | github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect 187 | github.com/tjfoc/gmsm v1.3.2 // indirect 188 | github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect 189 | github.com/transparency-dev/merkle v0.0.1 // indirect 190 | github.com/urfave/cli v1.22.7 // indirect 191 | github.com/vbatts/tar-split v0.11.2 // indirect 192 | github.com/xanzy/go-gitlab v0.73.1 // indirect 193 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect 194 | github.com/zclconf/go-cty v1.10.0 // indirect 195 | github.com/zeebo/errs v1.2.2 // indirect 196 | go.etcd.io/bbolt v1.3.6 // indirect 197 | go.etcd.io/etcd/api/v3 v3.6.0-alpha.0 // indirect 198 | go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0 // indirect 199 | go.etcd.io/etcd/client/v2 v2.306.0-alpha.0 // indirect 200 | go.etcd.io/etcd/client/v3 v3.6.0-alpha.0 // indirect 201 | go.etcd.io/etcd/etcdctl/v3 v3.6.0-alpha.0 // indirect 202 | go.etcd.io/etcd/etcdutl/v3 v3.6.0-alpha.0 // indirect 203 | go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0 // indirect 204 | go.etcd.io/etcd/raft/v3 v3.6.0-alpha.0 // indirect 205 | go.etcd.io/etcd/server/v3 v3.6.0-alpha.0 // indirect 206 | go.etcd.io/etcd/tests/v3 v3.6.0-alpha.0 // indirect 207 | go.etcd.io/etcd/v3 v3.6.0-alpha.0 // indirect 208 | go.mongodb.org/mongo-driver v1.10.0 // indirect 209 | go.opencensus.io v0.24.0 // indirect 210 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 // indirect 211 | go.opentelemetry.io/otel v1.7.0 // indirect 212 | go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0 // indirect 213 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0 // indirect 214 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.7.0 // indirect 215 | go.opentelemetry.io/otel/sdk v1.7.0 // indirect 216 | go.opentelemetry.io/otel/trace v1.7.0 // indirect 217 | go.opentelemetry.io/proto/otlp v0.16.0 // indirect 218 | go.uber.org/atomic v1.10.0 // indirect 219 | go.uber.org/multierr v1.8.0 // indirect 220 | go.uber.org/zap v1.23.0 // indirect 221 | golang.org/x/crypto v0.5.0 // indirect 222 | golang.org/x/exp v0.0.0-20220823124025-807a23277127 // indirect 223 | golang.org/x/mod v0.6.0 // indirect 224 | golang.org/x/net v0.5.0 // indirect 225 | golang.org/x/oauth2 v0.4.0 // indirect 226 | golang.org/x/sync v0.1.0 // indirect 227 | golang.org/x/term v0.4.0 // indirect 228 | golang.org/x/text v0.6.0 // indirect 229 | golang.org/x/time v0.2.0 // indirect 230 | golang.org/x/tools v0.1.12 // indirect 231 | google.golang.org/api v0.107.0 // indirect 232 | google.golang.org/appengine v1.6.7 // indirect 233 | google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef // indirect 234 | google.golang.org/grpc v1.51.0 // indirect 235 | google.golang.org/protobuf v1.28.1 // indirect 236 | gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect 237 | gopkg.in/inf.v0 v0.9.1 // indirect 238 | gopkg.in/ini.v1 v1.67.0 // indirect 239 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect 240 | gopkg.in/square/go-jose.v2 v2.6.0 // indirect 241 | gopkg.in/yaml.v2 v2.4.0 // indirect 242 | k8s.io/api v0.23.5 // indirect 243 | k8s.io/apimachinery v0.23.5 // indirect 244 | k8s.io/client-go v0.23.5 // indirect 245 | k8s.io/klog/v2 v2.60.1-0.20220317184644-43cc75f9ae89 // indirect 246 | k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf // indirect 247 | k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect 248 | sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect 249 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 250 | sigs.k8s.io/yaml v1.3.0 // indirect 251 | ) 252 | 253 | require ( 254 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 255 | github.com/shibumi/go-pathspec v1.3.0 // indirect 256 | github.com/spf13/pflag v1.0.5 // indirect 257 | github.com/stretchr/testify v1.8.1 258 | golang.org/x/sys v0.4.0 // indirect 259 | ) 260 | --------------------------------------------------------------------------------