|
| 1 | +/* |
| 2 | +Copyright 2023 The OpenVEX Authors |
| 3 | +SPDX-License-Identifier: Apache-2.0 |
| 4 | +*/ |
| 5 | + |
| 6 | +package cmd |
| 7 | + |
| 8 | +import ( |
| 9 | + "errors" |
| 10 | + "fmt" |
| 11 | + |
| 12 | + "github.com/sirupsen/logrus" |
| 13 | + "github.com/spf13/cobra" |
| 14 | + |
| 15 | + "github.com/openvex/go-vex/pkg/vex" |
| 16 | + "github.com/openvex/vexctl/pkg/ctl" |
| 17 | +) |
| 18 | + |
| 19 | +type generateOptions struct { |
| 20 | + vexDocOptions |
| 21 | + outFileOption |
| 22 | + Product string |
| 23 | + TemplatesPath string |
| 24 | + Init bool |
| 25 | +} |
| 26 | + |
| 27 | +// Validates the options in context with arguments |
| 28 | +func (o *generateOptions) Validate() error { |
| 29 | + var err, errInit error |
| 30 | + if o.Product == "" && !o.Init { |
| 31 | + err = errors.New("a required product id is needed to generate a valid VEX statement") |
| 32 | + } |
| 33 | + |
| 34 | + if o.Init && o.Product != "" { |
| 35 | + errInit = errors.New("when specifying --init, no product can be set") |
| 36 | + } |
| 37 | + |
| 38 | + return errors.Join( |
| 39 | + err, errInit, |
| 40 | + o.outFileOption.Validate(), |
| 41 | + o.vexDocOptions.Validate(), |
| 42 | + ) |
| 43 | +} |
| 44 | + |
| 45 | +func (o *generateOptions) AddFlags(cmd *cobra.Command) { |
| 46 | + o.vexDocOptions.AddFlags(cmd) |
| 47 | + o.outFileOption.AddFlags(cmd) |
| 48 | + |
| 49 | + cmd.PersistentFlags().StringVarP( |
| 50 | + &o.Product, |
| 51 | + productLongFlag, |
| 52 | + "p", |
| 53 | + "", |
| 54 | + "main identifier of the product, a package URL or another IRI", |
| 55 | + ) |
| 56 | + |
| 57 | + cmd.PersistentFlags().StringVarP( |
| 58 | + &o.TemplatesPath, |
| 59 | + "templates", |
| 60 | + "t", |
| 61 | + ctl.DefaultTemplatesPath, |
| 62 | + "path to templates directory", |
| 63 | + ) |
| 64 | + |
| 65 | + cmd.PersistentFlags().BoolVar( |
| 66 | + &o.Init, |
| 67 | + "init", |
| 68 | + false, |
| 69 | + "initialize a new templates directory in the path specified with -t", |
| 70 | + ) |
| 71 | +} |
| 72 | + |
| 73 | +func addGenerate(parentCmd *cobra.Command) { |
| 74 | + opts := generateOptions{} |
| 75 | + generateCmd := &cobra.Command{ |
| 76 | + Short: fmt.Sprintf("%s generate: generates VEX data", appname), |
| 77 | + Long: fmt.Sprintf(`%s generate: generates VEX data from golden templates |
| 78 | +
|
| 79 | +The generate subcommand reads a set of golden template files and |
| 80 | +creates a new document for a new artifact based on the golden samples. |
| 81 | +
|
| 82 | +To start, create your golden templates directory. You can initialize a new |
| 83 | +templates directory using the --init flag: |
| 84 | +
|
| 85 | +vexctl generate --init --templates=".openvex/templates" |
| 86 | +
|
| 87 | +That invocation will create a new directory and add a new empty openvex document. |
| 88 | +You can add more statements to it with "vexctl add" (see vexctl add --help). |
| 89 | +
|
| 90 | +The golden templates are normal OpenVEX documents. Their only difference is that |
| 91 | +statements have generic identifiers that will be included in the generated data |
| 92 | +when matched by a more specific data. |
| 93 | +
|
| 94 | +For example, to create a golden template for an OCI image, add a product with |
| 95 | +an unversioned purl like this: |
| 96 | +
|
| 97 | +"statements": [ |
| 98 | + { |
| 99 | + "vulnerability": { "name": "CVE-1234-5678" }, |
| 100 | + "products": [ |
| 101 | + { "@id": "pkg:oci/test" } |
| 102 | + ], |
| 103 | + "status": "fixed", |
| 104 | + "timestamp": "2023-12-05T05:04:34.77929922Z" |
| 105 | + } |
| 106 | +], |
| 107 | +
|
| 108 | +You can add that statement using the following invocation: |
| 109 | +
|
| 110 | +vexctl add --in-place main.openvex.json "pkg:oci/test" "CVE-1234-5678" fixed |
| 111 | +
|
| 112 | +The added statement will cause vexctl to generate a VEX document with data for |
| 113 | +CVE-1234-5678 for every version of the test image. In other words, when generating |
| 114 | +VEX data, products identified by these purls will get a statement with a status of |
| 115 | +fixed: |
| 116 | +
|
| 117 | + # Versioned purl (image with digest) |
| 118 | + pkg:oci/test@sha256%%3Af87abf1735e79b70407288f665316644d414dbf7bdf38c2f1c8e3a541d304d84 |
| 119 | +
|
| 120 | + # Image with tag |
| 121 | + pkg:oci/test?tag=latest |
| 122 | +
|
| 123 | + # Image with tag and repository |
| 124 | + pkg:oci/test?tag=latest&repository_url=ghcr.io%%2Fopenvex |
| 125 | +
|
| 126 | +Examples: |
| 127 | +
|
| 128 | +# Generate a document with all data for the an image with the reference |
| 129 | +# ghcr.io/openvex/test@sha256:f87abf1735e79b70407288f665316644d414dbf7bdf38c2f1c8e3a541d304d84 |
| 130 | +
|
| 131 | +%s generate --templates=".openvex/templates/" \ |
| 132 | + --product="pkg:oci/test@sha256%%3Af87abf1735e79b70407288f665316644d414dbf7bdf38c2f1c8e3a541d304d84&repository_url=ghcr.io%%2Fopenvex" |
| 133 | +
|
| 134 | +With that invocation, %s will read all template data from a directory |
| 135 | +called .openvex/templates, filter out the relevant statements and generate a |
| 136 | +VEX document for the specified product (the test image). |
| 137 | +
|
| 138 | +Note, that in this iteration, %s can only match products, subcomponents |
| 139 | +are not considered when filtering out statements. |
| 140 | +
|
| 141 | +You can customize the metadata of the document via the command line flags. |
| 142 | +%s will honor the SOURCE_DATE_EPOCH environment variable and use that date for |
| 143 | +the document (it can be formatted in UNIX time or RFC3339). |
| 144 | +
|
| 145 | +If you don't specify an ID for the document, one will be generated |
| 146 | +using its canonicalization hash. |
| 147 | +
|
| 148 | +`, appname, appname, appname, appname, appname), |
| 149 | + Use: "generate [flags] [product_id]", |
| 150 | + Example: fmt.Sprintf("%s generate \"pkg:apk/wolfi/git", appname), |
| 151 | + SilenceUsage: false, |
| 152 | + SilenceErrors: true, |
| 153 | + PersistentPreRunE: initLogging, |
| 154 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 155 | + if len(args) > 0 { |
| 156 | + if opts.Product != "" && opts.Product != args[0] { |
| 157 | + return errors.New("product can only be specified once") |
| 158 | + } |
| 159 | + opts.Product = args[0] |
| 160 | + } |
| 161 | + |
| 162 | + if err := opts.Validate(); err != nil { |
| 163 | + return err |
| 164 | + } |
| 165 | + |
| 166 | + // Options are relatively simple for now |
| 167 | + genopts := ctl.GenerateOpts{ |
| 168 | + TemplatesPath: opts.TemplatesPath, |
| 169 | + } |
| 170 | + |
| 171 | + vexctl := ctl.New() |
| 172 | + |
| 173 | + // If initializing, do that and exit |
| 174 | + if opts.Init { |
| 175 | + if err := vexctl.InitTemplatesDirectory(&genopts); err != nil { |
| 176 | + return fmt.Errorf("initializing templates directory: %w", err) |
| 177 | + } |
| 178 | + logrus.Infof("Initialized new templates directory in %s", genopts.TemplatesPath) |
| 179 | + return nil |
| 180 | + } |
| 181 | + |
| 182 | + newDoc, err := vexctl.Generate(&genopts, []*vex.Product{ |
| 183 | + {Component: vex.Component{ID: opts.Product}}, |
| 184 | + }) |
| 185 | + if err != nil { |
| 186 | + return fmt.Errorf("generating VEX data: %w", err) |
| 187 | + } |
| 188 | + |
| 189 | + if newDoc == nil { |
| 190 | + logrus.Warnf("No VEX data found for %s", opts.Product) |
| 191 | + return nil |
| 192 | + } |
| 193 | + |
| 194 | + newDoc.Metadata.Author = opts.Author |
| 195 | + newDoc.Metadata.AuthorRole = opts.AuthorRole |
| 196 | + |
| 197 | + if opts.DocumentID != "" { |
| 198 | + newDoc.Metadata.ID = opts.DocumentID |
| 199 | + } |
| 200 | + |
| 201 | + if err := writeDocument(newDoc, opts.outFilePath); err != nil { |
| 202 | + return fmt.Errorf("writing openvex document: %w", err) |
| 203 | + } |
| 204 | + return nil |
| 205 | + }, |
| 206 | + } |
| 207 | + |
| 208 | + opts.AddFlags(generateCmd) |
| 209 | + parentCmd.AddCommand(generateCmd) |
| 210 | +} |
0 commit comments