From 2d24426a459654f024fa824da6871447108e70ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Thu, 14 Dec 2023 22:08:20 -0600 Subject: [PATCH 1/8] Add ReadTemplateData() function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a function to read the golden templates to the ctl implementation Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/ctl/implementation.go | 54 ++++++++++++++++++++++++++++++++++ pkg/ctl/implementation_test.go | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/pkg/ctl/implementation.go b/pkg/ctl/implementation.go index f55dcdc..5c135e9 100644 --- a/pkg/ctl/implementation.go +++ b/pkg/ctl/implementation.go @@ -13,6 +13,8 @@ import ( "encoding/json" "errors" "fmt" + "os" + "path/filepath" "regexp" "sort" "strings" @@ -52,6 +54,7 @@ type Implementation interface { ListDocumentProducts(doc *vex.VEX) ([]productRef, error) NormalizeProducts([]productRef) ([]productRef, []productRef, []productRef, error) VerifyImageSubjects(*attestation.Attestation, *vex.VEX) error + ReadTemplateData(*GenerateOpts, []*vex.Product) (*vex.VEX, error) } type defaultVexCtlImplementation struct{} @@ -601,3 +604,54 @@ func (impl *defaultVexCtlImplementation) VerifyImageSubjects( } return nil } + +// ReadTemplateData reads a set of golden documents with data used to generate +// VEX information for a given artifact. +func (impl *defaultVexCtlImplementation) ReadTemplateData(opts *GenerateOpts, products []*vex.Product) (*vex.VEX, error) { + goldenPath := opts.TemplatesPath + if goldenPath == "" { + goldenPath = defaultTemplatesPath + } + + info, err := os.Stat(goldenPath) + if err != nil { + return nil, fmt.Errorf("checking filepath: %w", err) + } + + vexFiles := []string{} + if info.IsDir() { + entries, err := os.ReadDir(goldenPath) + if err != nil { + return nil, fmt.Errorf("reading golden data directory: %w", err) + } + + for _, f := range entries { + vexFiles = append(vexFiles, filepath.Join(goldenPath, f.Name())) + } + } else { + vexFiles = []string{goldenPath} + } + + // The VEX options only support matching products with a string. + // We unpack all the product data and match on it + productsIdentifiers := []string{} + for _, p := range products { + productsIdentifiers = append(productsIdentifiers, p.ID) + for _, id := range p.Identifiers { + productsIdentifiers = append(productsIdentifiers, id) + } + for _, h := range p.Hashes { + productsIdentifiers = append(productsIdentifiers, string(h)) + } + } + + // Generate the full VEX history + document, err := vex.MergeFilesWithOptions(&vex.MergeOptions{ + Products: productsIdentifiers, + }, vexFiles) + if err != nil { + return nil, fmt.Errorf("merging golden data: %w", err) + } + + return document, nil +} diff --git a/pkg/ctl/implementation_test.go b/pkg/ctl/implementation_test.go index b9ea142..9726b95 100644 --- a/pkg/ctl/implementation_test.go +++ b/pkg/ctl/implementation_test.go @@ -324,3 +324,55 @@ func TestMerge(t *testing.T) { }) } } + +func TestReadGoldenData(t *testing.T) { + sut := defaultVexCtlImplementation{} + for _, tc := range []struct { + name string + opts *GenerateOpts + products []*vex.Product + expectedLen int + mustErr bool + }{ + { + name: "same identifier", + opts: &GenerateOpts{ + TemplatesPath: "testdata/templates/", + }, + products: []*vex.Product{ + {Component: vex.Component{ID: "pkg:oci/vexctl"}}, + }, + expectedLen: 1, + }, + { + name: "no matching purl", + opts: &GenerateOpts{ + TemplatesPath: "testdata/templates/", + }, + products: []*vex.Product{ + {Component: vex.Component{ID: "pkg:oci/otherimage"}}, + }, + expectedLen: 0, + }, + { + name: "versioned purl", + opts: &GenerateOpts{ + TemplatesPath: "testdata/templates/", + }, + products: []*vex.Product{ + {Component: vex.Component{ID: "pkg:oci/vexctl@sha256%3Af87abf1735e79b70407288f665316644d414dbf7bdf38c2f1c8e3a541d304d84"}}, + }, + expectedLen: 1, + }, + } { + t.Run(tc.name, func(t *testing.T) { + doc, err := sut.ReadTemplateData(tc.opts, tc.products) + if tc.mustErr { + require.Error(t, err) + return + } + require.NotNil(t, doc) + require.Len(t, doc.Statements, tc.expectedLen, "unexpected number of statements") + }) + } +} From 24fe0e9e1c8cc103e4119b78e305902fe3b5d77a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Thu, 14 Dec 2023 23:44:07 -0600 Subject: [PATCH 2/8] Add Generate method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds a generate() method to vexctl that generates /data from a golden templates set Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/ctl/ctl.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pkg/ctl/ctl.go b/pkg/ctl/ctl.go index fa8003d..7f286e4 100644 --- a/pkg/ctl/ctl.go +++ b/pkg/ctl/ctl.go @@ -211,3 +211,25 @@ func (vexctl *VexCtl) MergeFiles(ctx context.Context, opts *MergeOptions, filePa } return doc, nil } + +type GenerateOpts struct { + // TemplatesPath is a file or directory containing the OpenVEX files to be + // used as templates to generate the data. + TemplatesPath string +} + +const defaultTemplatesPath = ".openvex/templates" + +// Generate generates the upt to date vex data about an artifact from information +// captured in golden VEX documents. +func (vexctl *VexCtl) Generate(opts *GenerateOpts, products []*vex.Product) (*vex.VEX, error) { + // Read the golden data files. This returns a vex document with all + // statements applicable to the products + doc, err := vexctl.impl.ReadTemplateData(opts, products) + if err != nil { + return nil, fmt.Errorf("reading template data: %w", err) + } + + // TODO(puerco): Normalize identifiers + return doc, nil +} From 52953e7b1ab43b4d27018f2f87a83b74882e6b7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Fri, 15 Dec 2023 00:42:33 -0600 Subject: [PATCH 3/8] vexctl generate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit adds the vexctl generate command. Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/main.go | 1 + pkg/ctl/ctl.go | 2 +- pkg/ctl/implementation.go | 7 ++++++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/cmd/main.go b/internal/cmd/main.go index 5386ff5..0f790ad 100644 --- a/internal/cmd/main.go +++ b/internal/cmd/main.go @@ -55,6 +55,7 @@ func init() { addCreate(rootCmd) addList(rootCmd) addAdd(rootCmd) + addGenerate(rootCmd) rootCmd.AddCommand(version.WithFont("doom")) } diff --git a/pkg/ctl/ctl.go b/pkg/ctl/ctl.go index 7f286e4..f577851 100644 --- a/pkg/ctl/ctl.go +++ b/pkg/ctl/ctl.go @@ -218,7 +218,7 @@ type GenerateOpts struct { TemplatesPath string } -const defaultTemplatesPath = ".openvex/templates" +const DefaultTemplatesPath = ".openvex/templates" // Generate generates the upt to date vex data about an artifact from information // captured in golden VEX documents. diff --git a/pkg/ctl/implementation.go b/pkg/ctl/implementation.go index 5c135e9..5caa59a 100644 --- a/pkg/ctl/implementation.go +++ b/pkg/ctl/implementation.go @@ -610,7 +610,7 @@ func (impl *defaultVexCtlImplementation) VerifyImageSubjects( func (impl *defaultVexCtlImplementation) ReadTemplateData(opts *GenerateOpts, products []*vex.Product) (*vex.VEX, error) { goldenPath := opts.TemplatesPath if goldenPath == "" { - goldenPath = defaultTemplatesPath + goldenPath = DefaultTemplatesPath } info, err := os.Stat(goldenPath) @@ -632,6 +632,11 @@ func (impl *defaultVexCtlImplementation) ReadTemplateData(opts *GenerateOpts, pr vexFiles = []string{goldenPath} } + // If we have no files, then noop + if len(vexFiles) == 0 { + return nil, nil + } + // The VEX options only support matching products with a string. // We unpack all the product data and match on it productsIdentifiers := []string{} From b514dc8531010a09ce4277c7fc10c06948a60c4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Fri, 15 Dec 2023 00:45:58 -0600 Subject: [PATCH 4/8] Only read openvex files as templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/ctl/implementation.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/ctl/implementation.go b/pkg/ctl/implementation.go index 5caa59a..34bb1e4 100644 --- a/pkg/ctl/implementation.go +++ b/pkg/ctl/implementation.go @@ -626,6 +626,9 @@ func (impl *defaultVexCtlImplementation) ReadTemplateData(opts *GenerateOpts, pr } for _, f := range entries { + if !strings.HasSuffix(f.Name(), "vex.json") { + continue + } vexFiles = append(vexFiles, filepath.Join(goldenPath, f.Name())) } } else { From 0a05ac3c9b28b132b393e8132ea27d08a7e4cb92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Fri, 15 Dec 2023 01:33:54 -0600 Subject: [PATCH 5/8] Add generate --init flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commimt adds a --init flag that creates a new tempaltes directory Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/ctl/ctl.go | 5 +++ pkg/ctl/implementation.go | 65 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/pkg/ctl/ctl.go b/pkg/ctl/ctl.go index f577851..31d594b 100644 --- a/pkg/ctl/ctl.go +++ b/pkg/ctl/ctl.go @@ -233,3 +233,8 @@ func (vexctl *VexCtl) Generate(opts *GenerateOpts, products []*vex.Product) (*ve // TODO(puerco): Normalize identifiers return doc, nil } + +// InitTemplatesDirectory initializes a new templates directory +func (vexctl *VexCtl) InitTemplatesDirectory(opts *GenerateOpts) error { + return vexctl.impl.InitTemplatesDir(opts.TemplatesPath) +} diff --git a/pkg/ctl/implementation.go b/pkg/ctl/implementation.go index 34bb1e4..7233511 100644 --- a/pkg/ctl/implementation.go +++ b/pkg/ctl/implementation.go @@ -38,7 +38,31 @@ import ( "github.com/openvex/vexctl/pkg/attestation" ) -const IntotoPayloadType = "application/vnd.in-toto+json" +const ( + IntotoPayloadType = "application/vnd.in-toto+json" + + initReadmeMarkdown = "# OpenVEX Templates Directory\n\n" + + "This directory contains the OpenVEX data for this repository.\n" + + "The files stored in this directory are used as templates by\n" + + "`vexctl generate` when generating VEX data for a release or \n" + + "a specific artifact.\n\n" + + "To add new statements to publish data about a vulnerability,\n" + + "download [vexctl](https://github.com/openvex/vexctl)\n" + + "and append new statements using `vexctl add`. For example:\n" + + "```\n" + + "vexctl add --in-place main.openvex.json pkg:oci/test CVE-2014-1234567 fixed\n" + + "```\n" + + "That will add a new VEX statement expressing that the impact of\n" + + "CVE-2014-1234567 is under investigation in the test image. When\n" + + "cutting a new release, for `pkg:oci/test` the new file will be\n" + + "incorporated to the relase's VEX data.\n\n" + + "## Read more about OpenVEX\n\n" + + "To know more about generating, publishing and using VEX data\n" + + "in your project, please check out the vexctl repository and\n" + + "documentation: https://github.com/openvex/vexctl\n\n" + + "OpenVEX also has an examples repository with samples and docs:\n" + + "https://github.com/openvex/examples\n\n" +) type Implementation interface { ApplySingleVEX(*sarif.Report, *vex.VEX) (*sarif.Report, error) @@ -55,6 +79,7 @@ type Implementation interface { NormalizeProducts([]productRef) ([]productRef, []productRef, []productRef, error) VerifyImageSubjects(*attestation.Attestation, *vex.VEX) error ReadTemplateData(*GenerateOpts, []*vex.Product) (*vex.VEX, error) + InitTemplatesDir(string) error } type defaultVexCtlImplementation struct{} @@ -663,3 +688,41 @@ func (impl *defaultVexCtlImplementation) ReadTemplateData(opts *GenerateOpts, pr return document, nil } + +// InitTemplatesDir initializes the templates directory with an emptuy file and +// a readme. +func (impl *defaultVexCtlImplementation) InitTemplatesDir(path string) error { + if !util.Exists(path) { + if err := os.MkdirAll(path, os.FileMode(0o755)); err != nil { + return fmt.Errorf("creating templates dir: %s", err) + } + } + + entries, err := os.ReadDir(path) + if err != nil { + return fmt.Errorf("reading templates dir: %w", err) + } + + if len(entries) != 0 { + return fmt.Errorf("unable to initialize templates dir, path is not empty") + } + + mainFile, err := os.Create(filepath.Join(path, "main.openvex.json")) + if err != nil { + return fmt.Errorf("creating initial openvex document: %w", err) + } + newDoc := vex.New() + newDoc.Metadata.Author = "vexctl (automated template)" + // TODO(puerco) This should be randomized + if _, err := newDoc.GenerateCanonicalID(); err != nil { + return fmt.Errorf("generating document ID: %w", err) + } + if err := newDoc.ToJSON(mainFile); err != nil { + return fmt.Errorf("writing initial openvex file to disk: %w", err) + } + + if err := os.WriteFile(filepath.Join(path, "README.md"), []byte(initReadmeMarkdown), os.FileMode(0o644)); err != nil { + return fmt.Errorf("writing OpenVEX template dir readme file: %w", err) + } + return nil +} From dddf079f229378ef7ad7aea700d887bc40abc898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Fri, 15 Dec 2023 01:35:32 -0600 Subject: [PATCH 6/8] Add generate --init test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/ctl/implementation_test.go | 37 ++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/pkg/ctl/implementation_test.go b/pkg/ctl/implementation_test.go index 9726b95..85d3ebb 100644 --- a/pkg/ctl/implementation_test.go +++ b/pkg/ctl/implementation_test.go @@ -7,6 +7,8 @@ package ctl import ( "context" + "os" + "path/filepath" "testing" intoto "github.com/in-toto/in-toto-golang/in_toto" @@ -376,3 +378,38 @@ func TestReadGoldenData(t *testing.T) { }) } } + +func TestInitTemplatesDir(t *testing.T) { + sut := defaultVexCtlImplementation{} + for _, tc := range []struct { + name string + prepare func(string) + shouldErr bool + }{ + { + name: "normal", + prepare: func(s string) {}, + shouldErr: false, + }, + { + name: "not clean dir", + prepare: func(s string) { + require.NoError(t, os.WriteFile(filepath.Join(s, "test.txt"), []byte("abc"), os.FileMode(0o644))) + }, + shouldErr: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + tc.prepare(dir) + err := sut.InitTemplatesDir(dir) + if tc.shouldErr { + require.Error(t, err) + return + } + require.FileExists(t, filepath.Join(dir, "README.md")) + require.FileExists(t, filepath.Join(dir, "main.openvex.json")) + require.NoError(t, err) + }) + } +} From 448944af8c205b162d4ee94e80fa265283a87a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Fri, 15 Dec 2023 01:44:31 -0600 Subject: [PATCH 7/8] Add generate subcommand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- internal/cmd/generate.go | 210 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 internal/cmd/generate.go diff --git a/internal/cmd/generate.go b/internal/cmd/generate.go new file mode 100644 index 0000000..6c57634 --- /dev/null +++ b/internal/cmd/generate.go @@ -0,0 +1,210 @@ +/* +Copyright 2023 The OpenVEX Authors +SPDX-License-Identifier: Apache-2.0 +*/ + +package cmd + +import ( + "errors" + "fmt" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + + "github.com/openvex/go-vex/pkg/vex" + "github.com/openvex/vexctl/pkg/ctl" +) + +type generateOptions struct { + vexDocOptions + outFileOption + Product string + TemplatesPath string + Init bool +} + +// Validates the options in context with arguments +func (o *generateOptions) Validate() error { + var err, errInit error + if o.Product == "" && !o.Init { + err = errors.New("a required product id is needed to generate a valid VEX statement") + } + + if o.Init && o.Product != "" { + errInit = errors.New("when specifying --init, no product can be set") + } + + return errors.Join( + err, errInit, + o.outFileOption.Validate(), + o.vexDocOptions.Validate(), + ) +} + +func (o *generateOptions) AddFlags(cmd *cobra.Command) { + o.vexDocOptions.AddFlags(cmd) + o.outFileOption.AddFlags(cmd) + + cmd.PersistentFlags().StringVarP( + &o.Product, + productLongFlag, + "p", + "", + "main identifier of the product, a package URL or another IRI", + ) + + cmd.PersistentFlags().StringVarP( + &o.TemplatesPath, + "templates", + "t", + ctl.DefaultTemplatesPath, + "path to templates directory", + ) + + cmd.PersistentFlags().BoolVar( + &o.Init, + "init", + false, + "initialize a new templates directory in the path specified with -t", + ) +} + +func addGenerate(parentCmd *cobra.Command) { + opts := generateOptions{} + generateCmd := &cobra.Command{ + Short: fmt.Sprintf("%s generate: generates VEX data", appname), + Long: fmt.Sprintf(`%s generate: generates VEX data from golden templates + +The generate subcommand reads a set of golden template files and +creates a new document for a new artifact based on the golden samples. + +To start, create your golden templates directory. You can initialize a new +templates directory using the --init flag: + +vexctl generate --init --templates=".openvex/templates" + +That invocation will create a new directory and add a new empty openvex document. +You can add more statements to it with "vexctl add" (see vexctl add --help). + +The golden templates are normal OpenVEX documents. Their only difference is that +statements have generic identifiers that will be included in the generated data +when matched by a more specific data. + +For example, to create a golden template for an OCI image, add a product with +an unversioned purl like this: + +"statements": [ + { + "vulnerability": { "name": "CVE-1234-5678" }, + "products": [ + { "@id": "pkg:oci/test" } + ], + "status": "fixed", + "timestamp": "2023-12-05T05:04:34.77929922Z" + } +], + +You can add that statement using the following invocation: + +vexctl add --in-place main.openvex.json "pkg:oci/test" "CVE-1234-5678" fixed + +The added statement will cause vexctl to generate a VEX document with data for +CVE-1234-5678 for every version of the test image. In other words, when generating +VEX data, products identified by these purls will get a statement with a status of +fixed: + + # Versioned purl (image with digest) + pkg:oci/test@sha256%%3Af87abf1735e79b70407288f665316644d414dbf7bdf38c2f1c8e3a541d304d84 + + # Image with tag + pkg:oci/test?tag=latest + + # Image with tag and repository + pkg:oci/test?tag=latest&repository_url=ghcr.io%%2Fopenvex + +Examples: + +# Generate a document with all data for the an image with the reference +# ghcr.io/openvex/test@sha256:f87abf1735e79b70407288f665316644d414dbf7bdf38c2f1c8e3a541d304d84 + +%s generate --templates=".openvex/templates/" \ + --product="pkg:oci/test@sha256%%3Af87abf1735e79b70407288f665316644d414dbf7bdf38c2f1c8e3a541d304d84&repository_url=ghcr.io%%2Fopenvex" + +With that invocation, %s will read all template data from a directory +called .openvex/templates, filter out the relevant statements and generate a +VEX document for the specified product (the test image). + +Note, that in this iteration, %s can only match products, subcomponents +are not considered when filtering out statements. + +You can customize the metadata of the document via the command line flags. +%s will honor the SOURCE_DATE_EPOCH environment variable and use that date for +the document (it can be formatted in UNIX time or RFC3339). + +If you don't specify an ID for the document, one will be generated +using its canonicalization hash. + +`, appname, appname, appname, appname, appname), + Use: "generate [flags] [product_id]", + Example: fmt.Sprintf("%s generate \"pkg:apk/wolfi/git", appname), + SilenceUsage: false, + SilenceErrors: true, + PersistentPreRunE: initLogging, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) > 0 { + if opts.Product != "" && opts.Product != args[0] { + return errors.New("product can only be specified once") + } + opts.Product = args[0] + } + + if err := opts.Validate(); err != nil { + return err + } + + // Options are relatively simple for now + genopts := ctl.GenerateOpts{ + TemplatesPath: opts.TemplatesPath, + } + + vexctl := ctl.New() + + // If initializing, do that and exit + if opts.Init { + if err := vexctl.InitTemplatesDirectory(&genopts); err != nil { + return fmt.Errorf("initializing templates directory: %w", err) + } + logrus.Infof("Initialized new templates directory in %s", genopts.TemplatesPath) + return nil + } + + newDoc, err := vexctl.Generate(&genopts, []*vex.Product{ + {Component: vex.Component{ID: opts.Product}}, + }) + if err != nil { + return fmt.Errorf("generating VEX data: %w", err) + } + + if newDoc == nil { + logrus.Warnf("No VEX data found for %s", opts.Product) + return nil + } + + newDoc.Metadata.Author = opts.Author + newDoc.Metadata.AuthorRole = opts.AuthorRole + + if opts.DocumentID != "" { + newDoc.Metadata.ID = opts.DocumentID + } + + if err := writeDocument(newDoc, opts.outFilePath); err != nil { + return fmt.Errorf("writing openvex document: %w", err) + } + return nil + }, + } + + opts.AddFlags(generateCmd) + parentCmd.AddCommand(generateCmd) +} From 5f44388aa313a91d16d4c41f9aa4236a7e36a93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Fri, 15 Dec 2023 01:44:53 -0600 Subject: [PATCH 8/8] Add generate test fixtures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- .../templates/oci-vexctl.openvex.json | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 pkg/ctl/testdata/templates/oci-vexctl.openvex.json diff --git a/pkg/ctl/testdata/templates/oci-vexctl.openvex.json b/pkg/ctl/testdata/templates/oci-vexctl.openvex.json new file mode 100644 index 0000000..7245115 --- /dev/null +++ b/pkg/ctl/testdata/templates/oci-vexctl.openvex.json @@ -0,0 +1,22 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://openvex.dev/docs/public/vex-4d0ddb41ceda9b98b3fb2d17c5a1b8e264a0c1aca03c726cf0486f7e602f65d2", + "author": "Unknown Author", + "timestamp": "2023-12-14T17:25:52.227708106-06:00", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-2014-12345678" + }, + "timestamp": "2023-12-14T17:25:52.227708816-06:00", + "products": [ + { + "@id": "pkg:oci/vexctl" + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present" + } + ] +}