Skip to content

Commit 13fa934

Browse files
authored
Merge pull request #150 from puerco/generate
vexctl generate
2 parents df4eab0 + 5f44388 commit 13fa934

File tree

6 files changed

+475
-1
lines changed

6 files changed

+475
-1
lines changed

internal/cmd/generate.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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+
}

internal/cmd/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ func init() {
5555
addCreate(rootCmd)
5656
addList(rootCmd)
5757
addAdd(rootCmd)
58+
addGenerate(rootCmd)
5859
rootCmd.AddCommand(version.WithFont("doom"))
5960
}
6061

pkg/ctl/ctl.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,30 @@ func (vexctl *VexCtl) MergeFiles(ctx context.Context, opts *MergeOptions, filePa
211211
}
212212
return doc, nil
213213
}
214+
215+
type GenerateOpts struct {
216+
// TemplatesPath is a file or directory containing the OpenVEX files to be
217+
// used as templates to generate the data.
218+
TemplatesPath string
219+
}
220+
221+
const DefaultTemplatesPath = ".openvex/templates"
222+
223+
// Generate generates the upt to date vex data about an artifact from information
224+
// captured in golden VEX documents.
225+
func (vexctl *VexCtl) Generate(opts *GenerateOpts, products []*vex.Product) (*vex.VEX, error) {
226+
// Read the golden data files. This returns a vex document with all
227+
// statements applicable to the products
228+
doc, err := vexctl.impl.ReadTemplateData(opts, products)
229+
if err != nil {
230+
return nil, fmt.Errorf("reading template data: %w", err)
231+
}
232+
233+
// TODO(puerco): Normalize identifiers
234+
return doc, nil
235+
}
236+
237+
// InitTemplatesDirectory initializes a new templates directory
238+
func (vexctl *VexCtl) InitTemplatesDirectory(opts *GenerateOpts) error {
239+
return vexctl.impl.InitTemplatesDir(opts.TemplatesPath)
240+
}

0 commit comments

Comments
 (0)