-
Notifications
You must be signed in to change notification settings - Fork 58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
advisory: move command #1042
base: main
Are you sure you want to change the base?
advisory: move command #1042
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
## wolfictl advisory move | ||
|
||
Move a package's advisories into a new package. | ||
|
||
***Aliases**: mv* | ||
|
||
### Usage | ||
|
||
``` | ||
wolfictl advisory move <old-package-name> <new-package-name> | ||
``` | ||
|
||
### Synopsis | ||
|
||
Move a package's advisories into a new package. | ||
|
||
This command will move most advisories for the given package into a new package. And rename the | ||
package to the new package name. (i.e., from foo.advisories.yaml to foo-X.Y.advisories.yaml) If the | ||
target file already exists, the command will try to merge the advisories. To ensure the advisories | ||
are up-to-date, the command will start a scan for the new package. | ||
|
||
This command is also useful to start version streaming for an existing package that has not been | ||
version streamed before. Especially that requires manual intervention to move the advisories. | ||
|
||
The command will move the latest event for each advisory, and will update the timestamp | ||
of the event to now. The command will not copy events of type "detection", "fixed", | ||
"analysis_not_planned", or "fix_not_planned". | ||
|
||
|
||
### Options | ||
|
||
``` | ||
-d, --dir string directory containing the advisories to copy (default ".") | ||
-h, --help help for move | ||
``` | ||
|
||
### Options inherited from parent commands | ||
|
||
``` | ||
--log-level string log level (e.g. debug, info, warn, error) (default "info") | ||
--log-policy strings log policy (e.g. builtin:stderr, /tmp/log/foo) (default [builtin:stderr]) | ||
``` | ||
|
||
### SEE ALSO | ||
|
||
* [wolfictl advisory](wolfictl_advisory.md) - Commands for consuming and maintaining security advisory data | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
.TH "WOLFICTL\-ADVISORY\-MOVE" "1" "" "Auto generated by spf13/cobra" "" | ||
.nh | ||
.ad l | ||
|
||
|
||
.SH NAME | ||
.PP | ||
wolfictl\-advisory\-move \- Move a package's advisories into a new package. | ||
|
||
|
||
.SH SYNOPSIS | ||
.PP | ||
\fBwolfictl advisory move <old-package-name> <new-package-name>\fP | ||
|
||
|
||
.SH DESCRIPTION | ||
.PP | ||
Move a package's advisories into a new package. | ||
|
||
.PP | ||
This command will move most advisories for the given package into a new package. And rename the | ||
package to the new package name. (i.e., from foo.advisories.yaml to foo\-X.Y.advisories.yaml) If the | ||
target file already exists, the command will try to merge the advisories. To ensure the advisories | ||
are up\-to\-date, the command will start a scan for the new package. | ||
|
||
.PP | ||
This command is also useful to start version streaming for an existing package that has not been | ||
version streamed before. Especially that requires manual intervention to move the advisories. | ||
|
||
.PP | ||
The command will move the latest event for each advisory, and will update the timestamp | ||
of the event to now. The command will not copy events of type "detection", "fixed", | ||
"analysis\_not\_planned", or "fix\_not\_planned". | ||
|
||
|
||
.SH OPTIONS | ||
.PP | ||
\fB\-d\fP, \fB\-\-dir\fP="." | ||
directory containing the advisories to copy | ||
|
||
.PP | ||
\fB\-h\fP, \fB\-\-help\fP[=false] | ||
help for move | ||
|
||
|
||
.SH OPTIONS INHERITED FROM PARENT COMMANDS | ||
.PP | ||
\fB\-\-log\-level\fP="info" | ||
log level (e.g. debug, info, warn, error) | ||
|
||
.PP | ||
\fB\-\-log\-policy\fP=[builtin:stderr] | ||
log policy (e.g. builtin:stderr, /tmp/log/foo) | ||
|
||
|
||
.SH SEE ALSO | ||
.PP | ||
\fBwolfictl\-advisory(1)\fP |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,39 +49,9 @@ of the event to now. The command will not copy events of type "detection", "fixe | |
out.Advisories = nil | ||
|
||
for _, adv := range hdoc.Advisories { | ||
evts := make([]v2.Event, 0, len(adv.Events)) | ||
|
||
for _, evt := range adv.Events { | ||
switch evt.Type { | ||
case v2.EventTypeDetection, v2.EventTypeFixed, v2.EventTypeAnalysisNotPlanned, v2.EventTypeFixNotPlanned: | ||
// Don't carry these over. | ||
continue | ||
|
||
case v2.EventTypePendingUpstreamFix, v2.EventTypeFalsePositiveDetermination, v2.EventTypeTruePositiveDetermination: | ||
// Carry these over as-is. | ||
evts = append(evts, evt) | ||
|
||
default: | ||
// A new type was added and we don't know how to handle it. Default to not carrying it over. | ||
} | ||
} | ||
|
||
if len(evts) == 0 { | ||
// No events to carry over. | ||
continue | ||
if carried, ok := carryAdvisory(adv); ok { | ||
out.Advisories = append(out.Advisories, carried) | ||
} | ||
|
||
// Sort events by timestamp and only take the latest event. | ||
sort.Slice(evts, func(i, j int) bool { | ||
return evts[i].Timestamp.Before(evts[j].Timestamp) | ||
}) | ||
evts = []v2.Event{evts[len(evts)-1]} | ||
|
||
// Update the timestamp to now. | ||
evts[0].Timestamp = v2.Now() | ||
|
||
adv.Events = evts | ||
out.Advisories = append(out.Advisories, adv) | ||
} | ||
|
||
return advisoryCfgs.Create(ctx, want+".advisories.yaml", out) | ||
|
@@ -91,3 +61,42 @@ of the event to now. The command will not copy events of type "detection", "fixe | |
|
||
return cmd | ||
} | ||
|
||
// carryAdvisory decides whether to carry over an advisory and its events. | ||
// Returns true with the updated advisory if it should be carried over. Otherwise, returns false | ||
// and the current advisory. | ||
func carryAdvisory(advisory v2.Advisory) (v2.Advisory, bool) { | ||
evts := make([]v2.Event, 0, len(advisory.Events)) | ||
|
||
for _, evt := range advisory.Events { | ||
switch evt.Type { | ||
case v2.EventTypeDetection, v2.EventTypeFixed, v2.EventTypeAnalysisNotPlanned, v2.EventTypeFixNotPlanned: | ||
// Don't carry these over. | ||
continue | ||
|
||
case v2.EventTypePendingUpstreamFix, v2.EventTypeFalsePositiveDetermination, v2.EventTypeTruePositiveDetermination: | ||
// Carry these over as-is. | ||
evts = append(evts, evt) | ||
|
||
default: | ||
// A new type was added and we don't know how to handle it. Default to not carrying it over. | ||
} | ||
} | ||
|
||
if len(evts) == 0 { | ||
// No events to carry over. | ||
return advisory, false | ||
} | ||
|
||
// Sort events by timestamp and only take the latest event. | ||
sort.Slice(evts, func(i, j int) bool { | ||
return evts[i].Timestamp.Before(evts[j].Timestamp) | ||
}) | ||
evts = []v2.Event{evts[len(evts)-1]} | ||
|
||
// Update the timestamp to now. | ||
evts[0].Timestamp = v2.Now() | ||
|
||
advisory.Events = evts | ||
Comment on lines
+95
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: This feels a little awkward to me. Could we capture the event we want, then do the timestamp update, and then just return a slice that includes that event? (It seems strange to operate on the event within the slice when we don't need to) |
||
return advisory, true | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
package cli | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/spf13/cobra" | ||
v2 "github.com/wolfi-dev/wolfictl/pkg/configs/advisory/v2" | ||
rwos "github.com/wolfi-dev/wolfictl/pkg/configs/rwfs/os" | ||
) | ||
|
||
func cmdAdvisoryMove() *cobra.Command { | ||
var dir string | ||
cmd := &cobra.Command{ | ||
Use: "move <old-package-name> <new-package-name>", | ||
Aliases: []string{"mv"}, | ||
Short: "Move a package's advisories into a new package.", | ||
Long: `Move a package's advisories into a new package. | ||
|
||
This command will move most advisories for the given package into a new package. And rename the | ||
package to the new package name. (i.e., from foo.advisories.yaml to foo-X.Y.advisories.yaml) If the | ||
target file already exists, the command will try to merge the advisories. To ensure the advisories | ||
are up-to-date, the command will start a scan for the new package. | ||
|
||
This command is also useful to start version streaming for an existing package that has not been | ||
version streamed before. Especially that requires manual intervention to move the advisories. | ||
|
||
The command will move the latest event for each advisory, and will update the timestamp | ||
of the event to now. The command will not copy events of type "detection", "fixed", | ||
"analysis_not_planned", or "fix_not_planned". | ||
`, | ||
SilenceErrors: true, | ||
Args: cobra.ExactArgs(2), | ||
RunE: func(cmd *cobra.Command, args []string) error { | ||
ctx := cmd.Context() | ||
have, want := args[0], args[1] | ||
|
||
have = strings.TrimSuffix(have, ".advisories.yaml") | ||
want = strings.TrimSuffix(want, ".advisories.yaml") | ||
|
||
advisoryFsys := rwos.DirFS(dir) | ||
advisoryCfgs, err := v2.NewIndex(ctx, advisoryFsys) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
oldEntry, err := advisoryCfgs.Select().WhereName(have).First() | ||
if err != nil { | ||
return fmt.Errorf("unable to find advisory for package %q: %w", have, err) | ||
} | ||
oldDoc := oldEntry.Configuration() | ||
|
||
shouldMergeExistings := false | ||
newEntry, err := advisoryCfgs.Select().WhereName(want).First() | ||
if err == nil && len(newEntry.Configuration().Advisories) > 0 { | ||
shouldMergeExistings = true | ||
} | ||
|
||
out := *oldDoc | ||
out.Package.Name = want | ||
out.Advisories = nil | ||
|
||
for _, adv := range oldDoc.Advisories { | ||
if carried, ok := carryAdvisory(adv); ok { | ||
out.Advisories = append(out.Advisories, carried) | ||
} | ||
} | ||
|
||
havePath := have + ".advisories.yaml" | ||
wantPath := want + ".advisories.yaml" | ||
|
||
// If the new file already exists, merge the old advisories to it and re-create. | ||
if shouldMergeExistings { | ||
newDoc := newEntry.Configuration() | ||
|
||
updater := v2.NewAdvisoriesSectionUpdater(func(_ v2.Document) (v2.Advisories, error) { | ||
return mergeExistingAdvisories(out.Advisories, newDoc.Advisories), nil | ||
}) | ||
|
||
if err := newEntry.Update(ctx, updater); err != nil { | ||
return fmt.Errorf("unable to update %q: %w", wantPath, err) | ||
} | ||
|
||
// Remove the existing file to re-create it since it's already existed. | ||
if err := advisoryCfgs.Remove(wantPath); err != nil { | ||
return fmt.Errorf("unable to remove old file %q: %w", wantPath, err) | ||
} | ||
} | ||
|
||
if err := advisoryCfgs.Remove(havePath); err != nil { | ||
return fmt.Errorf("unable to remove old file %q: %w", havePath, err) | ||
} | ||
|
||
return advisoryCfgs.Create(ctx, wantPath, out) | ||
}, | ||
} | ||
cmd.PersistentFlags().StringVarP(&dir, "dir", "d", ".", "directory containing the advisories to copy") | ||
|
||
return cmd | ||
} | ||
|
||
// mergeExistingAdvisories merges the current advisories with the existing advisories. | ||
func mergeExistingAdvisories(current, existing v2.Advisories) v2.Advisories { | ||
res := make(v2.Advisories, 0, len(current)+len(existing)) | ||
|
||
// Add current advisories to the result and mark their IDs as seen | ||
res = append(res, current...) | ||
|
||
// Add existing advisories to the result if they are not already present | ||
for _, adv := range existing { | ||
if _, found := res.Get(adv.ID); !found { | ||
res = append(res, adv) | ||
} | ||
} | ||
|
||
return res | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,3 +32,39 @@ func TestNewIndex(t *testing.T) { | |
assert.NotContains(t, index.paths, ".not-a-config.yaml") | ||
}) | ||
} | ||
|
||
func TestRemove(t *testing.T) { | ||
ctx := context.Background() | ||
fsys := rwos.DirFS("testdata/index-1") | ||
|
||
index, err := NewIndex[config.Configuration](ctx, fsys, func(ctx context.Context, path string) (*config.Configuration, error) { | ||
return config.ParseConfiguration(ctx, path, config.WithFS(fsys)) | ||
}) | ||
require.NoError(t, err) | ||
|
||
name := "config-new" | ||
filename := name + ".advisories.yaml" | ||
|
||
err = index.Create(ctx, filename, config.Configuration{ | ||
Package: config.Package{ | ||
Name: name, | ||
Version: "1.0.0", | ||
}, | ||
}) | ||
require.NoError(t, err) | ||
|
||
_, err = index.Select().WhereName(name).First() | ||
require.NoError(t, err) | ||
|
||
t.Run("removes a config", func(t *testing.T) { | ||
err := index.Remove(filename) | ||
require.NoError(t, err) | ||
|
||
assert.NotContains(t, index.paths, filename) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this line for? It looks like we're testing the removal in the test block below? |
||
}) | ||
|
||
t.Run("ensure the config is removed", func(t *testing.T) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also test that other configs weren't removed and are still available? |
||
_, err := index.Select().WhereName(name).First() | ||
require.Error(t, err) | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this PR still needs some cleanup from our last discussion?