From 67fb1220455d181ae1c1edae01fcad3d4b793203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 13 Mar 2024 09:35:22 -0600 Subject: [PATCH 1/4] Checkin of the mindpak libraries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/mindpak/build/packer.go | 174 ++++++++ pkg/mindpak/bundle.go | 163 +++++++ pkg/mindpak/bundle_test.go | 109 +++++ pkg/mindpak/manifest.go | 42 ++ pkg/mindpak/manifest_test.go | 87 ++++ pkg/mindpak/mindpack.pb.go | 415 ++++++++++++++++++ .../t1/profiles/branch-protection.yaml | 84 ++++ .../t1/rule_types/secret_scanning.yaml | 104 +++++ pkg/mindpak/testdata/t2/manifest.json | 28 ++ .../t2/profiles/branch-protection.yaml | 84 ++++ .../rule_types/branch_protection_enabled.yaml | 78 ++++ 11 files changed, 1368 insertions(+) create mode 100644 pkg/mindpak/build/packer.go create mode 100644 pkg/mindpak/bundle.go create mode 100644 pkg/mindpak/bundle_test.go create mode 100644 pkg/mindpak/manifest.go create mode 100644 pkg/mindpak/manifest_test.go create mode 100644 pkg/mindpak/mindpack.pb.go create mode 100644 pkg/mindpak/testdata/t1/profiles/branch-protection.yaml create mode 100644 pkg/mindpak/testdata/t1/rule_types/secret_scanning.yaml create mode 100644 pkg/mindpak/testdata/t2/manifest.json create mode 100644 pkg/mindpak/testdata/t2/profiles/branch-protection.yaml create mode 100644 pkg/mindpak/testdata/t2/rule_types/branch_protection_enabled.yaml diff --git a/pkg/mindpak/build/packer.go b/pkg/mindpak/build/packer.go new file mode 100644 index 0000000000..ed23b155d4 --- /dev/null +++ b/pkg/mindpak/build/packer.go @@ -0,0 +1,174 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package build implements tools and function to build mindpaks. The main +// builder is build.Packer that writes the bundles to archives. +package build + +import ( + "archive/tar" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/stacklok/minder/pkg/mindpak" +) + +// Packer handles writing the bundles to archives on disk. +type Packer struct{} + +// NewPacker returns a new packer object with the default options +func NewPacker() *Packer { + return &Packer{} +} + +// InitOptions are used when initializing a new bundle directory. +type InitOptions struct { + *mindpak.Metadata + Path string +} + +// Validate checks the initializer options +func (opts *InitOptions) Validate() error { + var errs = []error{} + if opts.Name == "" { + errs = append(errs, fmt.Errorf("name is required to initialize a mindpack")) + } else if !mindpak.ValidNameRegex.MatchString(opts.Name) { + errs = append(errs, fmt.Errorf("%q is not a valid mindpack name", opts.Name)) + } + + if opts.Namespace != "" && !mindpak.ValidNameRegex.MatchString(opts.Namespace) { + errs = append(errs, fmt.Errorf("%q is not valida namespace", opts.Namespace)) + } + + // FIXME(puerco): Check semver + + // Check path + sdata, err := os.Stat(opts.Path) + if err != nil { + errs = append(errs, fmt.Errorf("opening path: %w", err)) + } else { + if !sdata.IsDir() { + errs = append(errs, fmt.Errorf("path is not a directory")) + } + } + + return errors.Join(errs...) +} + +// Init creates a new bundle manifest in a directory with minder data in the +// expected structure. +func (_ *Packer) Init(opts *InitOptions) error { + if opts.Metadata.Name == "" { + return fmt.Errorf("unable to initialize new bundle, no name defined") + } + + bundle, err := mindpak.NewBundleFromDirectory(opts.Path) + if err != nil { + return fmt.Errorf("reading source data: %w", err) + } + + bundle.Metadata = opts.Metadata + + if err := bundle.UpdateManifest(); err != nil { + return fmt.Errorf("updating new bundle manifest: %w", err) + } + + bundle.Metadata.Date = timestamppb.Now() + + f, err := os.Create(filepath.Join(opts.Path, mindpak.ManifestFileName)) + if err != nil { + return fmt.Errorf("opening manifest file: %w", err) + } + + if err := bundle.Manifest.Write(f); err != nil { + return fmt.Errorf("writing manifest data: %w", err) + } + + return nil +} + +// WriteToFile writes the bundle to a file on disk. +func (p *Packer) WriteToFile(bundle *mindpak.Bundle, path string) error { + path = filepath.Clean(path) + f, err := os.Create(path) + if err != nil { + return fmt.Errorf("opening file: %w", err) + } + defer f.Close() + + if err := p.Write(bundle, f); err != nil { + return fmt.Errorf("writing bundle to file: %w", err) + } + return nil +} + +// Write writes a bundle archive to writer w +func (_ *Packer) Write(bundle *mindpak.Bundle, w io.Writer) error { + tarWriter := tar.NewWriter(w) + defer tarWriter.Close() + + if bundle.Source == nil { + return fmt.Errorf("unable to pack bundle, data source not defined") + } + + err := fs.WalkDir(bundle.Source, ".", func(path string, _ fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("reading %q: %w", path, err) + } + + stat, err := fs.Stat(bundle.Source, path) + if err != nil { + return fmt.Errorf("reading file info: %w", err) + } + if stat.IsDir() { + return nil + } + + f, err := bundle.Source.Open(path) + if err != nil { + return fmt.Errorf("opening %q", path) + } + defer f.Close() + + header := &tar.Header{ + Name: path, + Size: stat.Size(), + Mode: int64(stat.Mode()), + ModTime: stat.ModTime(), + } + + if err := tarWriter.WriteHeader(header); err != nil { + return fmt.Errorf("writing header for %q: %w", path, err) + } + + if _, err := io.Copy(tarWriter, f); err != nil { + return fmt.Errorf("writing data from %q to archive: %w", path, err) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("walking bundle data source: %w", err) + } + + return nil +} diff --git a/pkg/mindpak/bundle.go b/pkg/mindpak/bundle.go new file mode 100644 index 0000000000..2f50ffa75d --- /dev/null +++ b/pkg/mindpak/bundle.go @@ -0,0 +1,163 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package mindpak abstracts to bundle profiles and rule types together in +// an artifact that allows for easy distribution and upgrade. +package mindpak + +import ( + "crypto/sha256" + "fmt" + "io" + "io/fs" + "os" + "regexp" + "strings" +) + +var ( + // ValidNameRegex is used to check a bundle name or namespace + ValidNameRegex = regexp.MustCompile(`^[a-zA-Z0-9](?:[-_a-zA-Z0-9]{0,61}[a-zA-Z0-9])?$`) +) + +const ( + // PathProfiles is the name of the directory holding the profiles of a bundle + PathProfiles = "profiles" + + // PathRuleTypes is the name of the directory holding the rule types of a bundle + PathRuleTypes = "rule_types" + + // ManifestFileName is the defaul filename for the manifest + ManifestFileName = "manifest.json" + + // SHA256 is the algorith name constant for the manifest and tests + SHA256 = "sha-256" +) + +// Bundle abstracts the bundle data. +// +// The bundle has a manifest. The manifest is composed by reading the +// Source filesystem and categorizing its entries. +type Bundle struct { + Manifest *Manifest + Metadata *Metadata + Files *Files + Source fs.StatFS +} + +// NewBundleFromDirectory reads a directory from a directory and returns +// a bundle loaded with its contents. The bundle will have its Source filesystem +// bound to the directory via an os.DirFS. +func NewBundleFromDirectory(path string) (*Bundle, error) { + bundle := &Bundle{ + Source: os.DirFS(path).(fs.StatFS), + } + if err := bundle.ReadSource(); err != nil { + return nil, fmt.Errorf("reading bundle data from %q: %w", path, err) + } + + return bundle, nil +} + +// UpdateManifest updates the bundle manifest to reflect the bundle data source +func (b *Bundle) UpdateManifest() error { + b.Manifest = &Manifest{ + Metadata: b.Metadata, + Files: b.Files, + } + return nil +} + +// ReadSource loads the data from the mindpak source filesystem +func (b *Bundle) ReadSource() error { + if b.Source == nil { + return fmt.Errorf("unable to read source, mindpak filesystem not defined") + } + + b.Manifest = &Manifest{ + Metadata: &Metadata{}, + Files: &Files{ + Profiles: []*File{}, + RuleTypes: []*File{}, + }, + } + + b.Files = &Files{ + Profiles: []*File{}, + RuleTypes: []*File{}, + } + + err := fs.WalkDir(b.Source, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("reading %q: %w", path, err) + } + if d.IsDir() { + return nil + } + + if !strings.HasPrefix(path, PathProfiles+"/") && + !strings.HasPrefix(path, PathRuleTypes+"/") && + !strings.HasPrefix(path, ManifestFileName) { + return fmt.Errorf("found unexpected entry in mindpak source: %q", path) + } + + f, err := b.Source.Open(path) + if err != nil { + return fmt.Errorf("opening %q", path) + } + defer f.Close() + + if path == ManifestFileName { + man := &Manifest{} + if err := man.Read(f); err != nil { + return fmt.Errorf("parsing manifest: %w", err) + } + b.Manifest = man + } + + h := sha256.New() + + if _, err := io.Copy(h, f); err != nil { + return fmt.Errorf("hashing %q", path) + } + + fentry := File{ + Name: d.Name(), + Hashes: map[string]string{ + "sha-256": fmt.Sprintf("%x", h.Sum(nil)), + }, + } + + switch { + case strings.HasPrefix(path, PathProfiles): + b.Files.Profiles = append(b.Files.Profiles, &fentry) + case strings.HasPrefix(path, PathRuleTypes): + b.Files.RuleTypes = append(b.Files.RuleTypes, &fentry) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("traversing bundle data source: %w", err) + } + return nil +} + +// Verify checks the contents of the bundle against its manifest +func (_ *Bundle) Verify() error { + // FIXME(puerco): Implement + return nil +} diff --git a/pkg/mindpak/bundle_test.go b/pkg/mindpak/bundle_test.go new file mode 100644 index 0000000000..dba993689b --- /dev/null +++ b/pkg/mindpak/bundle_test.go @@ -0,0 +1,109 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mindpak + +import ( + "io/fs" + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestReadSource(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + path string + mustErr bool + filesMatch bool + expect *Bundle + }{ + { + "normal", + "testdata/t1", + false, + true, + &Bundle{ + Manifest: &Manifest{}, + Metadata: &Metadata{}, + Files: &Files{ + Profiles: []*File{ + { + Name: "branch-protection.yaml", + Hashes: map[string]string{SHA256: "f3682a1cb5ab92c0cc71dd913338bf40a89ec324024f8d3f500be0e2aa4a9ae1"}, + }, + }, + RuleTypes: []*File{ + { + Name: "secret_scanning.yaml", + Hashes: map[string]string{SHA256: "572089a9a490d1b7d07f2a1f6845ae1f18af27a6a13a605de7cef8a910427084"}, + }, + }, + }, + Source: nil, + }, + }, + { + "wrong-hash", + "testdata/t1", + false, + false, + &Bundle{ + Manifest: &Manifest{}, + Metadata: &Metadata{}, + Files: &Files{ + Profiles: []*File{ + { + Name: "branch-protection.yaml", + Hashes: map[string]string{SHA256: "AAf3682a1cb5ab92c0cc71dd913338bf40a89ec324024f8d3f500be0e2aa4a9ae1"}, + }, + }, + RuleTypes: []*File{ + { + Name: "secret_scanning.yaml", + Hashes: map[string]string{SHA256: "AA572089a9a490d1b7d07f2a1f6845ae1f18af27a6a13a605de7cef8a910427084"}, + }, + }, + }, + Source: nil, + }, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + m := Bundle{} + m.Source = os.DirFS(tc.path).(fs.StatFS) + err := m.ReadSource() + if tc.mustErr { + require.Error(t, err) + return + } + + diff := cmp.Diff(&tc.expect.Files, &m.Files, protocmp.Transform()) + if tc.filesMatch { + require.Empty(t, diff, "file hashes don't match:\n%v", diff) + } else { + require.NotEmpty(t, diff, "file hashes should not match") + } + + }) + + } +} diff --git a/pkg/mindpak/manifest.go b/pkg/mindpak/manifest.go new file mode 100644 index 0000000000..7920fbe738 --- /dev/null +++ b/pkg/mindpak/manifest.go @@ -0,0 +1,42 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mindpak + +import ( + "encoding/json" + "fmt" + "io" +) + +// Write writes the bundle manifest to a file +func (m *Manifest) Write(w io.Writer) error { + e := json.NewEncoder(w) + e.SetIndent("", " ") + if err := e.Encode(&m); err != nil { + return fmt.Errorf("encoding bundle manifest: %w", err) + } + + return nil +} + +// Read loads the manifest data by parsing json data from reader r +func (m *Manifest) Read(r io.Reader) error { + dec := json.NewDecoder(r) + if err := dec.Decode(m); err != nil { + return fmt.Errorf("decoding manifest: %w", err) + } + return nil +} diff --git a/pkg/mindpak/manifest_test.go b/pkg/mindpak/manifest_test.go new file mode 100644 index 0000000000..c50fb9e4a0 --- /dev/null +++ b/pkg/mindpak/manifest_test.go @@ -0,0 +1,87 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package mindpak + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestManifestWrite(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + manifest *Manifest + mustErr bool + }{ + { + "normal", + &Manifest{ + Metadata: &Metadata{ + Name: "test", + Namespace: "testspace", + Version: "v1.2.0", + Date: ×tamppb.Timestamp{ + Seconds: 1709866805, + }, + }, + Files: &Files{ + Profiles: []*File{ + { + Name: "profile.yaml", + Hashes: map[string]string{ + SHA256: "8b438ca800dfa20c6ca66ed83f05ef874cc1e1859d1a0a193b4c0727e5629977", + }, + }, + }, + RuleTypes: []*File{ + { + Name: "rule_type.yaml", + Hashes: map[string]string{ + SHA256: "0aecaf4d7ce19dc39679952c6951005e1396a5e615289ff3deb351873957d055", + }, + }, + }, + }, + }, + false, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + b := bytes.NewBuffer([]byte{}) + err := tc.manifest.Write(b) + if tc.mustErr { + require.Error(t, err) + return + } + man := &Manifest{} + require.NoError(t, json.Unmarshal(b.Bytes(), man)) + + if diff := cmp.Diff(tc.manifest, man, protocmp.Transform()); diff != "" { + t.Fatalf("assertion failed: values are not equal\n--- expected\n+++ actual\n%v", diff) + } + }) + } + +} diff --git a/pkg/mindpak/mindpack.pb.go b/pkg/mindpak/mindpack.pb.go new file mode 100644 index 0000000000..988d0a5182 --- /dev/null +++ b/pkg/mindpak/mindpack.pb.go @@ -0,0 +1,415 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright 2024 Stacklok Inc + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.32.0 +// protoc (unknown) +// source: proto/mindpack/v1/mindpack.proto + +package mindpak + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Manifest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Files *Files `protobuf:"bytes,2,opt,name=files,proto3" json:"files,omitempty"` +} + +func (x *Manifest) Reset() { + *x = Manifest{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Manifest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Manifest) ProtoMessage() {} + +func (x *Manifest) ProtoReflect() protoreflect.Message { + mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Manifest.ProtoReflect.Descriptor instead. +func (*Manifest) Descriptor() ([]byte, []int) { + return file_proto_mindpack_v1_mindpack_proto_rawDescGZIP(), []int{0} +} + +func (x *Manifest) GetMetadata() *Metadata { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *Manifest) GetFiles() *Files { + if x != nil { + return x.Files + } + return nil +} + +type Metadata struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` + Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` + Date *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=date,proto3" json:"date,omitempty"` +} + +func (x *Metadata) Reset() { + *x = Metadata{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Metadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Metadata) ProtoMessage() {} + +func (x *Metadata) ProtoReflect() protoreflect.Message { + mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Metadata.ProtoReflect.Descriptor instead. +func (*Metadata) Descriptor() ([]byte, []int) { + return file_proto_mindpack_v1_mindpack_proto_rawDescGZIP(), []int{1} +} + +func (x *Metadata) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Metadata) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *Metadata) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *Metadata) GetDate() *timestamppb.Timestamp { + if x != nil { + return x.Date + } + return nil +} + +type File struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Hashes map[string]string `protobuf:"bytes,2,rep,name=hashes,proto3" json:"hashes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *File) Reset() { + *x = File{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *File) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*File) ProtoMessage() {} + +func (x *File) ProtoReflect() protoreflect.Message { + mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use File.ProtoReflect.Descriptor instead. +func (*File) Descriptor() ([]byte, []int) { + return file_proto_mindpack_v1_mindpack_proto_rawDescGZIP(), []int{2} +} + +func (x *File) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *File) GetHashes() map[string]string { + if x != nil { + return x.Hashes + } + return nil +} + +type Files struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Profiles []*File `protobuf:"bytes,1,rep,name=profiles,proto3" json:"profiles,omitempty"` + RuleTypes []*File `protobuf:"bytes,2,rep,name=ruleTypes,json=rule_types,proto3" json:"ruleTypes,omitempty"` +} + +func (x *Files) Reset() { + *x = Files{} + if protoimpl.UnsafeEnabled { + mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Files) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Files) ProtoMessage() {} + +func (x *Files) ProtoReflect() protoreflect.Message { + mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Files.ProtoReflect.Descriptor instead. +func (*Files) Descriptor() ([]byte, []int) { + return file_proto_mindpack_v1_mindpack_proto_rawDescGZIP(), []int{3} +} + +func (x *Files) GetProfiles() []*File { + if x != nil { + return x.Profiles + } + return nil +} + +func (x *Files) GetRuleTypes() []*File { + if x != nil { + return x.RuleTypes + } + return nil +} + +var File_proto_mindpack_v1_mindpack_proto protoreflect.FileDescriptor + +var file_proto_mindpack_v1_mindpack_proto_rawDesc = []byte{ + 0x0a, 0x20, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, + 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x0b, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x76, 0x31, 0x1a, + 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x22, 0x67, 0x0a, 0x08, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, + 0x28, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, + 0x2e, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6c, + 0x65, 0x73, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x86, 0x01, 0x0a, 0x08, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, + 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, + 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x64, 0x61, + 0x74, 0x65, 0x22, 0x8c, 0x01, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, + 0x35, 0x0a, 0x06, 0x68, 0x61, 0x73, 0x68, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1d, 0x2e, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, + 0x6c, 0x65, 0x2e, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, + 0x68, 0x61, 0x73, 0x68, 0x65, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x22, 0x68, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x08, 0x70, 0x72, + 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, + 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, + 0x08, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x09, 0x72, 0x75, 0x6c, + 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, + 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, + 0x0a, 0x72, 0x75, 0x6c, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x42, 0x29, 0x5a, 0x27, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x75, 0x65, 0x72, 0x63, 0x6f, + 0x2f, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x6d, 0x69, + 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proto_mindpack_v1_mindpack_proto_rawDescOnce sync.Once + file_proto_mindpack_v1_mindpack_proto_rawDescData = file_proto_mindpack_v1_mindpack_proto_rawDesc +) + +func file_proto_mindpack_v1_mindpack_proto_rawDescGZIP() []byte { + file_proto_mindpack_v1_mindpack_proto_rawDescOnce.Do(func() { + file_proto_mindpack_v1_mindpack_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_mindpack_v1_mindpack_proto_rawDescData) + }) + return file_proto_mindpack_v1_mindpack_proto_rawDescData +} + +var file_proto_mindpack_v1_mindpack_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_proto_mindpack_v1_mindpack_proto_goTypes = []interface{}{ + (*Manifest)(nil), // 0: mindpack.v1.Manifest + (*Metadata)(nil), // 1: mindpack.v1.Metadata + (*File)(nil), // 2: mindpack.v1.File + (*Files)(nil), // 3: mindpack.v1.Files + nil, // 4: mindpack.v1.File.HashesEntry + (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp +} +var file_proto_mindpack_v1_mindpack_proto_depIdxs = []int32{ + 1, // 0: mindpack.v1.Manifest.metadata:type_name -> mindpack.v1.Metadata + 3, // 1: mindpack.v1.Manifest.files:type_name -> mindpack.v1.Files + 5, // 2: mindpack.v1.Metadata.date:type_name -> google.protobuf.Timestamp + 4, // 3: mindpack.v1.File.hashes:type_name -> mindpack.v1.File.HashesEntry + 2, // 4: mindpack.v1.Files.profiles:type_name -> mindpack.v1.File + 2, // 5: mindpack.v1.Files.ruleTypes:type_name -> mindpack.v1.File + 6, // [6:6] is the sub-list for method output_type + 6, // [6:6] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_proto_mindpack_v1_mindpack_proto_init() } +func file_proto_mindpack_v1_mindpack_proto_init() { + if File_proto_mindpack_v1_mindpack_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proto_mindpack_v1_mindpack_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Manifest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_mindpack_v1_mindpack_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Metadata); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_mindpack_v1_mindpack_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*File); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proto_mindpack_v1_mindpack_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Files); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_proto_mindpack_v1_mindpack_proto_rawDesc, + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proto_mindpack_v1_mindpack_proto_goTypes, + DependencyIndexes: file_proto_mindpack_v1_mindpack_proto_depIdxs, + MessageInfos: file_proto_mindpack_v1_mindpack_proto_msgTypes, + }.Build() + File_proto_mindpack_v1_mindpack_proto = out.File + file_proto_mindpack_v1_mindpack_proto_rawDesc = nil + file_proto_mindpack_v1_mindpack_proto_goTypes = nil + file_proto_mindpack_v1_mindpack_proto_depIdxs = nil +} diff --git a/pkg/mindpak/testdata/t1/profiles/branch-protection.yaml b/pkg/mindpak/testdata/t1/profiles/branch-protection.yaml new file mode 100644 index 0000000000..1e387f9ec1 --- /dev/null +++ b/pkg/mindpak/testdata/t1/profiles/branch-protection.yaml @@ -0,0 +1,84 @@ +# +# Copyright 2024 Stacklok, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +# A profile to verify branch protection settings +version: v1 +type: profile +name: branch-protection-github-profile +context: + provider: github +alert: "off" +remediate: "off" +repository: + - type: branch_protection_enabled + params: + branch: "" + def: {} + - type: branch_protection_allow_deletions + params: + branch: "" + def: + allow_deletions: false + - type: branch_protection_allow_force_pushes + params: + branch: "" + def: + allow_force_pushes: false + - type: branch_protection_enforce_admins + params: + branch: "" + def: + enforce_admins: true + - type: branch_protection_lock_branch + params: + branch: "" + def: + lock_branch: false + - type: branch_protection_require_conversation_resolution + params: + branch: "" + def: + required_conversation_resolution: false + - type: branch_protection_require_pull_request_approving_review_count + params: + branch: "" + def: + required_approving_review_count: 1 + - type: branch_protection_require_pull_request_code_owners_review + params: + branch: "" + def: + require_code_owner_reviews: false + - type: branch_protection_require_pull_request_dismiss_stale_reviews + params: + branch: "" + def: + dismiss_stale_reviews: true + - type: branch_protection_require_pull_request_last_push_approval + params: + branch: "" + def: + require_last_push_approval: true + - type: branch_protection_require_pull_requests + params: + branch: "" + def: + required_pull_request_reviews: true + - type: branch_protection_require_signatures + params: + branch: "" + def: + required_signatures: false diff --git a/pkg/mindpak/testdata/t1/rule_types/secret_scanning.yaml b/pkg/mindpak/testdata/t1/rule_types/secret_scanning.yaml new file mode 100644 index 0000000000..d0c674241e --- /dev/null +++ b/pkg/mindpak/testdata/t1/rule_types/secret_scanning.yaml @@ -0,0 +1,104 @@ +# +# Copyright 2024 Stacklok, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +version: v1 +type: rule-type +name: secret_scanning +context: + provider: github +description: | + Verifies that secret scanning is enabled for a given repository. + Note that this will will not work as expected for private repositories + unless you have GitHub Advanced Security enabled. If you still want to use + this rule because you have a mixture of private and public repositories, + enable the `skip_private_repos` flag. +guidance: | + Secret scanning is a feature that scans repositories for secrets and alerts + the repository owner when a secret is found. To enable this feature in GitHub, + you must enable it in the repository settings. + + For more information, see + https://docs.github.com/en/github/administering-a-repository/about-secret-scanning +def: + # Defines the section of the pipeline the rule will appear in. + # This will affect the template used to render multiple parts + # of the rule. + in_entity: repository + # Defines the schema for writing a rule with this rule being checked + rule_schema: + properties: + enabled: + type: boolean + default: true + skip_private_repos: + type: boolean + default: true + description: | + If true, this rule will be marked as skipped for private repositories + # Defines the configuration for ingesting data relevant for the rule + ingest: + type: rest + rest: + # This is the path to the data source. Given that this will evaluate + # for each repository in the organization, we use a template that + # will be evaluated for each repository. The structure to use is the + # protobuf structure for the entity that is being evaluated. + endpoint: "/repos/{{.Entity.Owner}}/{{.Entity.Name}}" + # This is the method to use to retrieve the data. It should already default to JSON + parse: json + # Defines the configuration for evaluating data ingested against the given profile + eval: + type: rego + rego: + type: deny-by-default + def: | + package minder + + import future.keywords.if + + default allow := false + default skip := false + + allow if { + input.profile.enabled + input.ingested.security_and_analysis.secret_scanning.status == "enabled" + } + + allow if { + not input.profile.enabled + input.ingested.security_and_analysis.secret_scanning.status == "disabled" + } + + skip if { + input.profile.skip_private_repos == true + input.ingested.private == true + } + remediate: + type: rest + rest: + method: PATCH + endpoint: "/repos/{{.Entity.Owner}}/{{.Entity.Name}}" + body: | + {{- if .Profile.enabled }} + { "security_and_analysis": {"secret_scanning": { "status": "enabled" } } } + {{- else }} + { "security_and_analysis": {"secret_scanning": { "status": "disabled" } } } + {{- end }} + # Defines the configuration for alerting on the rule + alert: + type: security_advisory + security_advisory: + severity: "medium" diff --git a/pkg/mindpak/testdata/t2/manifest.json b/pkg/mindpak/testdata/t2/manifest.json new file mode 100644 index 0000000000..356c94528d --- /dev/null +++ b/pkg/mindpak/testdata/t2/manifest.json @@ -0,0 +1,28 @@ +{ + "metadata": { + "name": "t2", + "version": "v0.0.1", + "date": { + "seconds": 1709881152, + "nanos": 854906417 + } + }, + "files": { + "profiles": [ + { + "name": "branch-protection.yaml", + "hashes": { + "sha-256": "f3682a1cb5ab92c0cc71dd913338bf40a89ec324024f8d3f500be0e2aa4a9ae1" + } + } + ], + "ruleTypes": [ + { + "name": "branch_protection_enabled.yaml", + "hashes": { + "sha-256": "10198b8cac16cd1d983a0a6fbb950816448f65e8f1d7a7407e2ff94949b42ccb" + } + } + ] + } +} diff --git a/pkg/mindpak/testdata/t2/profiles/branch-protection.yaml b/pkg/mindpak/testdata/t2/profiles/branch-protection.yaml new file mode 100644 index 0000000000..1e387f9ec1 --- /dev/null +++ b/pkg/mindpak/testdata/t2/profiles/branch-protection.yaml @@ -0,0 +1,84 @@ +# +# Copyright 2024 Stacklok, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +# A profile to verify branch protection settings +version: v1 +type: profile +name: branch-protection-github-profile +context: + provider: github +alert: "off" +remediate: "off" +repository: + - type: branch_protection_enabled + params: + branch: "" + def: {} + - type: branch_protection_allow_deletions + params: + branch: "" + def: + allow_deletions: false + - type: branch_protection_allow_force_pushes + params: + branch: "" + def: + allow_force_pushes: false + - type: branch_protection_enforce_admins + params: + branch: "" + def: + enforce_admins: true + - type: branch_protection_lock_branch + params: + branch: "" + def: + lock_branch: false + - type: branch_protection_require_conversation_resolution + params: + branch: "" + def: + required_conversation_resolution: false + - type: branch_protection_require_pull_request_approving_review_count + params: + branch: "" + def: + required_approving_review_count: 1 + - type: branch_protection_require_pull_request_code_owners_review + params: + branch: "" + def: + require_code_owner_reviews: false + - type: branch_protection_require_pull_request_dismiss_stale_reviews + params: + branch: "" + def: + dismiss_stale_reviews: true + - type: branch_protection_require_pull_request_last_push_approval + params: + branch: "" + def: + require_last_push_approval: true + - type: branch_protection_require_pull_requests + params: + branch: "" + def: + required_pull_request_reviews: true + - type: branch_protection_require_signatures + params: + branch: "" + def: + required_signatures: false diff --git a/pkg/mindpak/testdata/t2/rule_types/branch_protection_enabled.yaml b/pkg/mindpak/testdata/t2/rule_types/branch_protection_enabled.yaml new file mode 100644 index 0000000000..1e88d55fb2 --- /dev/null +++ b/pkg/mindpak/testdata/t2/rule_types/branch_protection_enabled.yaml @@ -0,0 +1,78 @@ +# +# Copyright 2024 Stacklok, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- +version: v1 +type: rule-type +name: branch_protection_enabled +context: + provider: github +description: Verifies that a branch has a branch protection rule +guidance: | + You can protect important branches by setting branch protection rules, which define whether + collaborators can delete or force push to the branch and set requirements for any pushes to the branch, + such as passing status checks or a linear commit history. + + For more information, see + https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule +def: + # Defines the section of the pipeline the rule will appear in. + # This will affect the template used to render multiple parts + # of the rule. + in_entity: repository + # Defines the schema for parameters that will be passed to the rule + param_schema: + properties: + branch: + type: string + description: "The name of the branch to check. If left empty, the default branch will be used." + required: + - branch + rule_schema: {} + # Defines the configuration for ingesting data relevant for the rule + ingest: + type: rest + rest: + # This is the path to the data source. Given that this will evaluate + # for each repository in the organization, we use a template that + # will be evaluated for each repository. The structure to use is the + # protobuf structure for the entity that is being evaluated. + endpoint: '{{ $branch_param := index .Params "branch" }}/repos/{{.Entity.Owner}}/{{.Entity.Name}}/branches/{{if ne $branch_param "" }}{{ $branch_param }}{{ else }}{{ .Entity.DefaultBranch }}{{ end }}/protection' + # This is the method to use to retrieve the data. It should already default to JSON + parse: json + fallback: + - http_code: 404 + body: | + {"http_status": 404, "message": "Not Protected"} + eval: + type: rego + rego: + type: deny-by-default + def: | + package minder + + import future.keywords.every + import future.keywords.if + + default allow := false + + allow if { + input.ingested.url != "" + } + # Defines the configuration for alerting on the rule + alert: + type: security_advisory + security_advisory: + severity: "medium" From 282846d40878f89e6df230801e479a1bb3bc05d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 13 Mar 2024 13:26:44 -0600 Subject: [PATCH 2/4] Drop prototypes in favor of go native MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit implements native go types of the mindpak bundle structs Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/mindpak/build/packer.go | 7 +- pkg/mindpak/bundle.go | 18 +- pkg/mindpak/bundle_test.go | 8 +- pkg/mindpak/manifest.go | 6 + pkg/mindpak/manifest_test.go | 11 +- pkg/mindpak/mindpack.go | 59 +++++ pkg/mindpak/mindpack.pb.go | 415 ----------------------------------- 7 files changed, 79 insertions(+), 445 deletions(-) create mode 100644 pkg/mindpak/mindpack.go delete mode 100644 pkg/mindpak/mindpack.pb.go diff --git a/pkg/mindpak/build/packer.go b/pkg/mindpak/build/packer.go index ed23b155d4..27be243a32 100644 --- a/pkg/mindpak/build/packer.go +++ b/pkg/mindpak/build/packer.go @@ -25,8 +25,7 @@ import ( "io/fs" "os" "path/filepath" - - "google.golang.org/protobuf/types/known/timestamppb" + "time" "github.com/stacklok/minder/pkg/mindpak" ) @@ -90,8 +89,8 @@ func (_ *Packer) Init(opts *InitOptions) error { if err := bundle.UpdateManifest(); err != nil { return fmt.Errorf("updating new bundle manifest: %w", err) } - - bundle.Metadata.Date = timestamppb.Now() + t := time.Now() + bundle.Metadata.Date = &t f, err := os.Create(filepath.Join(opts.Path, mindpak.ManifestFileName)) if err != nil { diff --git a/pkg/mindpak/bundle.go b/pkg/mindpak/bundle.go index 2f50ffa75d..951f95c60d 100644 --- a/pkg/mindpak/bundle.go +++ b/pkg/mindpak/bundle.go @@ -32,20 +32,6 @@ var ( ValidNameRegex = regexp.MustCompile(`^[a-zA-Z0-9](?:[-_a-zA-Z0-9]{0,61}[a-zA-Z0-9])?$`) ) -const ( - // PathProfiles is the name of the directory holding the profiles of a bundle - PathProfiles = "profiles" - - // PathRuleTypes is the name of the directory holding the rule types of a bundle - PathRuleTypes = "rule_types" - - // ManifestFileName is the defaul filename for the manifest - ManifestFileName = "manifest.json" - - // SHA256 is the algorith name constant for the manifest and tests - SHA256 = "sha-256" -) - // Bundle abstracts the bundle data. // // The bundle has a manifest. The manifest is composed by reading the @@ -135,8 +121,8 @@ func (b *Bundle) ReadSource() error { fentry := File{ Name: d.Name(), - Hashes: map[string]string{ - "sha-256": fmt.Sprintf("%x", h.Sum(nil)), + Hashes: map[HashAlgorithm]string{ + SHA256: fmt.Sprintf("%x", h.Sum(nil)), }, } diff --git a/pkg/mindpak/bundle_test.go b/pkg/mindpak/bundle_test.go index dba993689b..d8ad7ceb52 100644 --- a/pkg/mindpak/bundle_test.go +++ b/pkg/mindpak/bundle_test.go @@ -46,13 +46,13 @@ func TestReadSource(t *testing.T) { Profiles: []*File{ { Name: "branch-protection.yaml", - Hashes: map[string]string{SHA256: "f3682a1cb5ab92c0cc71dd913338bf40a89ec324024f8d3f500be0e2aa4a9ae1"}, + Hashes: map[HashAlgorithm]string{SHA256: "51437d1e5049a16513b9cc9d6d93d6b25625f51e74e0861fba837cdf1d2b5f01"}, }, }, RuleTypes: []*File{ { Name: "secret_scanning.yaml", - Hashes: map[string]string{SHA256: "572089a9a490d1b7d07f2a1f6845ae1f18af27a6a13a605de7cef8a910427084"}, + Hashes: map[HashAlgorithm]string{SHA256: "fc3e782516d0de46e89610af0b0bab04783e0e6e875c6efa64c9dfb3ef127964"}, }, }, }, @@ -71,13 +71,13 @@ func TestReadSource(t *testing.T) { Profiles: []*File{ { Name: "branch-protection.yaml", - Hashes: map[string]string{SHA256: "AAf3682a1cb5ab92c0cc71dd913338bf40a89ec324024f8d3f500be0e2aa4a9ae1"}, + Hashes: map[HashAlgorithm]string{SHA256: "AAf3682a1cb5ab92c0cc71dd913338bf40a89ec324024f8d3f500be0e2aa4a9ae1"}, }, }, RuleTypes: []*File{ { Name: "secret_scanning.yaml", - Hashes: map[string]string{SHA256: "AA572089a9a490d1b7d07f2a1f6845ae1f18af27a6a13a605de7cef8a910427084"}, + Hashes: map[HashAlgorithm]string{SHA256: "AA572089a9a490d1b7d07f2a1f6845ae1f18af27a6a13a605de7cef8a910427084"}, }, }, }, diff --git a/pkg/mindpak/manifest.go b/pkg/mindpak/manifest.go index 7920fbe738..56b6ea817e 100644 --- a/pkg/mindpak/manifest.go +++ b/pkg/mindpak/manifest.go @@ -21,6 +21,12 @@ import ( "io" ) +// Manifest abstracts the json file included in the bundle that contains its metadata +type Manifest struct { + Metadata *Metadata `json:"metadata,omitempty"` + Files *Files `json:"files"` +} + // Write writes the bundle manifest to a file func (m *Manifest) Write(w io.Writer) error { e := json.NewEncoder(w) diff --git a/pkg/mindpak/manifest_test.go b/pkg/mindpak/manifest_test.go index c50fb9e4a0..f8cbc32933 100644 --- a/pkg/mindpak/manifest_test.go +++ b/pkg/mindpak/manifest_test.go @@ -19,15 +19,16 @@ import ( "bytes" "encoding/json" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" "google.golang.org/protobuf/testing/protocmp" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" ) func TestManifestWrite(t *testing.T) { t.Parallel() + now := time.Unix(1709866805, 0) for _, tc := range []struct { name string manifest *Manifest @@ -40,15 +41,13 @@ func TestManifestWrite(t *testing.T) { Name: "test", Namespace: "testspace", Version: "v1.2.0", - Date: ×tamppb.Timestamp{ - Seconds: 1709866805, - }, + Date: &now, }, Files: &Files{ Profiles: []*File{ { Name: "profile.yaml", - Hashes: map[string]string{ + Hashes: map[HashAlgorithm]string{ SHA256: "8b438ca800dfa20c6ca66ed83f05ef874cc1e1859d1a0a193b4c0727e5629977", }, }, @@ -56,7 +55,7 @@ func TestManifestWrite(t *testing.T) { RuleTypes: []*File{ { Name: "rule_type.yaml", - Hashes: map[string]string{ + Hashes: map[HashAlgorithm]string{ SHA256: "0aecaf4d7ce19dc39679952c6951005e1396a5e615289ff3deb351873957d055", }, }, diff --git a/pkg/mindpak/mindpack.go b/pkg/mindpak/mindpack.go new file mode 100644 index 0000000000..8a915a96e2 --- /dev/null +++ b/pkg/mindpak/mindpack.go @@ -0,0 +1,59 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package mindpak abstracts to bundle profiles and rule types together in +// an artifact that allows for easy distribution and upgrade. +package mindpak + +import "time" + +// HashAlgorithm is a label that indicates a hashing algorithm +type HashAlgorithm string + +const ( + // PathProfiles is the name of the directory holding the profiles of a bundle + PathProfiles = "profiles" + + // PathRuleTypes is the name of the directory holding the rule types of a bundle + PathRuleTypes = "rule_types" + + // ManifestFileName is the defaul filename for the manifest + ManifestFileName = "manifest.json" +) + +const ( + // SHA256 is the algorith name constant for the manifest and tests + SHA256 = HashAlgorithm("sha-256") +) + +// Metadata is the data describing the bundle +type Metadata struct { + Name string `json:"name,omitempty"` + Namespace string `json:"namespace,omitempty"` + Version string `json:"version,omitempty"` + Date *time.Time `json:"date,omitempty"` +} + +// File captures the name and hashes of a file included in the bundle +type File struct { + Name string `json:"name,omitempty"` + Hashes map[HashAlgorithm]string `json:"hashes,omitempty"` +} + +// Files is a collection of the files included in the bundle organized by type +type Files struct { + Profiles []*File `json:"profiles,omitempty"` + RuleTypes []*File `json:"ruleTypes,omitempty"` +} diff --git a/pkg/mindpak/mindpack.pb.go b/pkg/mindpak/mindpack.pb.go deleted file mode 100644 index 988d0a5182..0000000000 --- a/pkg/mindpak/mindpack.pb.go +++ /dev/null @@ -1,415 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: Copyright 2024 Stacklok Inc - -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.32.0 -// protoc (unknown) -// source: proto/mindpack/v1/mindpack.proto - -package mindpak - -import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - timestamppb "google.golang.org/protobuf/types/known/timestamppb" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type Manifest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - Files *Files `protobuf:"bytes,2,opt,name=files,proto3" json:"files,omitempty"` -} - -func (x *Manifest) Reset() { - *x = Manifest{} - if protoimpl.UnsafeEnabled { - mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Manifest) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Manifest) ProtoMessage() {} - -func (x *Manifest) ProtoReflect() protoreflect.Message { - mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Manifest.ProtoReflect.Descriptor instead. -func (*Manifest) Descriptor() ([]byte, []int) { - return file_proto_mindpack_v1_mindpack_proto_rawDescGZIP(), []int{0} -} - -func (x *Manifest) GetMetadata() *Metadata { - if x != nil { - return x.Metadata - } - return nil -} - -func (x *Manifest) GetFiles() *Files { - if x != nil { - return x.Files - } - return nil -} - -type Metadata struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` - Version string `protobuf:"bytes,3,opt,name=version,proto3" json:"version,omitempty"` - Date *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=date,proto3" json:"date,omitempty"` -} - -func (x *Metadata) Reset() { - *x = Metadata{} - if protoimpl.UnsafeEnabled { - mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Metadata) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Metadata) ProtoMessage() {} - -func (x *Metadata) ProtoReflect() protoreflect.Message { - mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Metadata.ProtoReflect.Descriptor instead. -func (*Metadata) Descriptor() ([]byte, []int) { - return file_proto_mindpack_v1_mindpack_proto_rawDescGZIP(), []int{1} -} - -func (x *Metadata) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *Metadata) GetNamespace() string { - if x != nil { - return x.Namespace - } - return "" -} - -func (x *Metadata) GetVersion() string { - if x != nil { - return x.Version - } - return "" -} - -func (x *Metadata) GetDate() *timestamppb.Timestamp { - if x != nil { - return x.Date - } - return nil -} - -type File struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Hashes map[string]string `protobuf:"bytes,2,rep,name=hashes,proto3" json:"hashes,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` -} - -func (x *File) Reset() { - *x = File{} - if protoimpl.UnsafeEnabled { - mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *File) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*File) ProtoMessage() {} - -func (x *File) ProtoReflect() protoreflect.Message { - mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use File.ProtoReflect.Descriptor instead. -func (*File) Descriptor() ([]byte, []int) { - return file_proto_mindpack_v1_mindpack_proto_rawDescGZIP(), []int{2} -} - -func (x *File) GetName() string { - if x != nil { - return x.Name - } - return "" -} - -func (x *File) GetHashes() map[string]string { - if x != nil { - return x.Hashes - } - return nil -} - -type Files struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Profiles []*File `protobuf:"bytes,1,rep,name=profiles,proto3" json:"profiles,omitempty"` - RuleTypes []*File `protobuf:"bytes,2,rep,name=ruleTypes,json=rule_types,proto3" json:"ruleTypes,omitempty"` -} - -func (x *Files) Reset() { - *x = Files{} - if protoimpl.UnsafeEnabled { - mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Files) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Files) ProtoMessage() {} - -func (x *Files) ProtoReflect() protoreflect.Message { - mi := &file_proto_mindpack_v1_mindpack_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Files.ProtoReflect.Descriptor instead. -func (*Files) Descriptor() ([]byte, []int) { - return file_proto_mindpack_v1_mindpack_proto_rawDescGZIP(), []int{3} -} - -func (x *Files) GetProfiles() []*File { - if x != nil { - return x.Profiles - } - return nil -} - -func (x *Files) GetRuleTypes() []*File { - if x != nil { - return x.RuleTypes - } - return nil -} - -var File_proto_mindpack_v1_mindpack_proto protoreflect.FileDescriptor - -var file_proto_mindpack_v1_mindpack_proto_rawDesc = []byte{ - 0x0a, 0x20, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, - 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x12, 0x0b, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x76, 0x31, 0x1a, - 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x22, 0x67, 0x0a, 0x08, 0x4d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, - 0x2e, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, - 0x28, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, - 0x2e, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6c, - 0x65, 0x73, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x86, 0x01, 0x0a, 0x08, 0x4d, 0x65, - 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x64, 0x61, - 0x74, 0x65, 0x22, 0x8c, 0x01, 0x0a, 0x04, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x35, 0x0a, 0x06, 0x68, 0x61, 0x73, 0x68, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x1d, 0x2e, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, - 0x6c, 0x65, 0x2e, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, - 0x68, 0x61, 0x73, 0x68, 0x65, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x48, 0x61, 0x73, 0x68, 0x65, 0x73, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x22, 0x68, 0x0a, 0x05, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x2d, 0x0a, 0x08, 0x70, 0x72, - 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, - 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, - 0x08, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x30, 0x0a, 0x09, 0x72, 0x75, 0x6c, - 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6d, - 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x52, - 0x0a, 0x72, 0x75, 0x6c, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x42, 0x29, 0x5a, 0x27, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x75, 0x65, 0x72, 0x63, 0x6f, - 0x2f, 0x6d, 0x69, 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x6d, 0x69, - 0x6e, 0x64, 0x70, 0x61, 0x63, 0x6b, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_proto_mindpack_v1_mindpack_proto_rawDescOnce sync.Once - file_proto_mindpack_v1_mindpack_proto_rawDescData = file_proto_mindpack_v1_mindpack_proto_rawDesc -) - -func file_proto_mindpack_v1_mindpack_proto_rawDescGZIP() []byte { - file_proto_mindpack_v1_mindpack_proto_rawDescOnce.Do(func() { - file_proto_mindpack_v1_mindpack_proto_rawDescData = protoimpl.X.CompressGZIP(file_proto_mindpack_v1_mindpack_proto_rawDescData) - }) - return file_proto_mindpack_v1_mindpack_proto_rawDescData -} - -var file_proto_mindpack_v1_mindpack_proto_msgTypes = make([]protoimpl.MessageInfo, 5) -var file_proto_mindpack_v1_mindpack_proto_goTypes = []interface{}{ - (*Manifest)(nil), // 0: mindpack.v1.Manifest - (*Metadata)(nil), // 1: mindpack.v1.Metadata - (*File)(nil), // 2: mindpack.v1.File - (*Files)(nil), // 3: mindpack.v1.Files - nil, // 4: mindpack.v1.File.HashesEntry - (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp -} -var file_proto_mindpack_v1_mindpack_proto_depIdxs = []int32{ - 1, // 0: mindpack.v1.Manifest.metadata:type_name -> mindpack.v1.Metadata - 3, // 1: mindpack.v1.Manifest.files:type_name -> mindpack.v1.Files - 5, // 2: mindpack.v1.Metadata.date:type_name -> google.protobuf.Timestamp - 4, // 3: mindpack.v1.File.hashes:type_name -> mindpack.v1.File.HashesEntry - 2, // 4: mindpack.v1.Files.profiles:type_name -> mindpack.v1.File - 2, // 5: mindpack.v1.Files.ruleTypes:type_name -> mindpack.v1.File - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name -} - -func init() { file_proto_mindpack_v1_mindpack_proto_init() } -func file_proto_mindpack_v1_mindpack_proto_init() { - if File_proto_mindpack_v1_mindpack_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_proto_mindpack_v1_mindpack_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Manifest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_proto_mindpack_v1_mindpack_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Metadata); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_proto_mindpack_v1_mindpack_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*File); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_proto_mindpack_v1_mindpack_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Files); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_proto_mindpack_v1_mindpack_proto_rawDesc, - NumEnums: 0, - NumMessages: 5, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_proto_mindpack_v1_mindpack_proto_goTypes, - DependencyIndexes: file_proto_mindpack_v1_mindpack_proto_depIdxs, - MessageInfos: file_proto_mindpack_v1_mindpack_proto_msgTypes, - }.Build() - File_proto_mindpack_v1_mindpack_proto = out.File - file_proto_mindpack_v1_mindpack_proto_rawDesc = nil - file_proto_mindpack_v1_mindpack_proto_goTypes = nil - file_proto_mindpack_v1_mindpack_proto_depIdxs = nil -} From fb056517e33f8baacc2208a5a04390d55966e93e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 13 Mar 2024 14:14:38 -0600 Subject: [PATCH 3/4] Add rest for bundle initiali[s|z]ation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- pkg/mindpak/build/packer.go | 14 +- pkg/mindpak/build/packer_test.go | 237 +++++++++++++++++++++++++++++++ pkg/mindpak/bundle.go | 8 ++ 3 files changed, 256 insertions(+), 3 deletions(-) create mode 100644 pkg/mindpak/build/packer_test.go diff --git a/pkg/mindpak/build/packer.go b/pkg/mindpak/build/packer.go index 27be243a32..e9eb8b5d76 100644 --- a/pkg/mindpak/build/packer.go +++ b/pkg/mindpak/build/packer.go @@ -72,9 +72,17 @@ func (opts *InitOptions) Validate() error { return errors.Join(errs...) } -// Init creates a new bundle manifest in a directory with minder data in the +// InitBundle creates a new bundle manifest in a directory with minder data in the // expected structure. -func (_ *Packer) Init(opts *InitOptions) error { +func (_ *Packer) InitBundle(opts *InitOptions) error { + if opts == nil { + return fmt.Errorf("invalid init options") + } + + if opts.Metadata == nil { + opts.Metadata = &mindpak.Metadata{} + } + if opts.Metadata.Name == "" { return fmt.Errorf("unable to initialize new bundle, no name defined") } @@ -100,7 +108,7 @@ func (_ *Packer) Init(opts *InitOptions) error { if err := bundle.Manifest.Write(f); err != nil { return fmt.Errorf("writing manifest data: %w", err) } - + fmt.Printf("wrote to %s", f.Name()) return nil } diff --git a/pkg/mindpak/build/packer_test.go b/pkg/mindpak/build/packer_test.go new file mode 100644 index 0000000000..d6265f7984 --- /dev/null +++ b/pkg/mindpak/build/packer_test.go @@ -0,0 +1,237 @@ +// +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package build implements tools and function to build mindpaks. The main +// builder is build.Packer that writes the bundles to archives. + +package build + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/stacklok/minder/pkg/mindpak" +) + +func TestPackerInitBundle(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + name string + opts *InitOptions + prepare func(*testing.T, *InitOptions) + mustErr bool + }{ + { + name: "nofiles", + opts: &InitOptions{ + Metadata: &mindpak.Metadata{ + Name: "my-bundle", + }, + }, + prepare: func(t *testing.T, opts *InitOptions) { + t.Helper() + d := t.TempDir() + opts.Path = d + }, + }, + { + name: "nofiles-withnamespace", + opts: &InitOptions{ + Metadata: &mindpak.Metadata{ + Name: "my-bundle", + Namespace: "ns", + }, + }, + prepare: func(t *testing.T, opts *InitOptions) { + t.Helper() + d := t.TempDir() + opts.Path = d + }, + }, + { + name: "files", + opts: &InitOptions{ + Metadata: &mindpak.Metadata{ + Name: "my-bundle", + Namespace: "ns", + }, + }, + prepare: func(t *testing.T, opts *InitOptions) { + t.Helper() + d := t.TempDir() + opts.Path = d + + require.NoError(t, os.Mkdir(filepath.Join(opts.Path, mindpak.PathProfiles), os.FileMode(0o700))) + require.NoError(t, os.Mkdir(filepath.Join(opts.Path, mindpak.PathRuleTypes), os.FileMode(0o700))) + + require.NoError(t, os.WriteFile(filepath.Join(opts.Path, mindpak.PathProfiles, "test1"), []byte("test"), os.FileMode(0o644))) + require.NoError(t, os.WriteFile(filepath.Join(opts.Path, mindpak.PathRuleTypes, "test2"), []byte("test2"), os.FileMode(0o644))) + }, + }, + { + name: "unexpected-files", + opts: &InitOptions{ + Metadata: &mindpak.Metadata{ + Name: "my-bundle", + Namespace: "ns", + }, + }, + prepare: func(t *testing.T, opts *InitOptions) { + t.Helper() + d := t.TempDir() + opts.Path = d + + require.NoError(t, os.Mkdir(filepath.Join(opts.Path, mindpak.PathProfiles), os.FileMode(0o700))) + require.NoError(t, os.Mkdir(filepath.Join(opts.Path, mindpak.PathRuleTypes), os.FileMode(0o700))) + + require.NoError(t, os.WriteFile(filepath.Join(opts.Path, mindpak.PathProfiles, "test1"), []byte("test"), os.FileMode(0o644))) + require.NoError(t, os.WriteFile(filepath.Join(opts.Path, mindpak.PathRuleTypes, "test2"), []byte("test2"), os.FileMode(0o644))) + require.NoError(t, os.WriteFile(filepath.Join(opts.Path, "hola"), []byte("test3"), os.FileMode(0o644))) + }, + mustErr: true, + }, + { + name: "noopts", + prepare: func(_ *testing.T, _ *InitOptions) {}, + mustErr: true, + }, + { + name: "noname", + opts: &InitOptions{}, + prepare: func(_ *testing.T, _ *InitOptions) {}, + mustErr: true, + }, + { + name: "invalid-dir", + opts: &InitOptions{ + Metadata: &mindpak.Metadata{ + Name: "my-bundle", + }, + Path: "my-dir", + }, + prepare: func(_ *testing.T, _ *InitOptions) {}, + mustErr: true, + }, + } { + tc := tc + t.Run(t.Name(), func(t *testing.T) { + t.Parallel() + + tc.prepare(t, tc.opts) + p := NewPacker() + + // Run the nundle initialization + err := p.InitBundle(tc.opts) + if tc.mustErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.FileExists(t, filepath.Join(tc.opts.Path, mindpak.ManifestFileName)) + }) + } +} + +func TestValidateInitOpts(t *testing.T) { + tmp := t.TempDir() + now := time.Now() + t.Parallel() + for _, tc := range []struct { + name string + opts *InitOptions + shouldErr bool + }{ + { + name: "noerror", + opts: &InitOptions{ + Metadata: &mindpak.Metadata{ + Name: "my-bundle", + Namespace: "ns", + Version: "1.0.0", + Date: &now, + }, + Path: tmp, + }, + shouldErr: false, + }, + { + name: "noname", + opts: &InitOptions{ + Metadata: &mindpak.Metadata{ + Name: "", + Namespace: "ns", + Version: "1.0.0", + Date: &now, + }, + Path: tmp, + }, + shouldErr: true, + }, + { + name: "invalid name", + opts: &InitOptions{ + Metadata: &mindpak.Metadata{ + Name: "it an invalid name!", + Namespace: "ns", + Version: "1.0.0", + Date: &now, + }, + Path: tmp, + }, + shouldErr: true, + }, + { + name: "invalid namespace", + opts: &InitOptions{ + Metadata: &mindpak.Metadata{ + Name: "name", + Namespace: "it an invalid namespace!", + Version: "1.0.0", + Date: &now, + }, + Path: tmp, + }, + shouldErr: true, + }, + { + name: "dir-notexists", + opts: &InitOptions{ + Metadata: &mindpak.Metadata{ + Name: "name", + Namespace: "ns", + Version: "1.0.0", + Date: &now, + }, + Path: "jklsdkjlsdljk sdkjl sd jkldsjkl", + }, + shouldErr: true, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + err := tc.opts.Validate() + if tc.shouldErr { + require.Error(t, err) + return + } + require.NoError(t, err) + }) + } +} diff --git a/pkg/mindpak/bundle.go b/pkg/mindpak/bundle.go index 951f95c60d..7e3cd9095f 100644 --- a/pkg/mindpak/bundle.go +++ b/pkg/mindpak/bundle.go @@ -47,6 +47,14 @@ type Bundle struct { // a bundle loaded with its contents. The bundle will have its Source filesystem // bound to the directory via an os.DirFS. func NewBundleFromDirectory(path string) (*Bundle, error) { + i, err := os.Stat(path) + if err != nil { + return nil, fmt.Errorf("opening bundle directory: %w", err) + } + if !i.IsDir() { + return nil, fmt.Errorf("specified path is not a directory") + } + bundle := &Bundle{ Source: os.DirFS(path).(fs.StatFS), } From 6a15a3dbbffd145650e4ef5f291f22e10d6c7422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20Garc=C3=ADa=20Veytia=20=28Puerco=29?= Date: Wed, 13 Mar 2024 14:45:16 -0600 Subject: [PATCH 4/4] go mod tidy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Adolfo García Veytia (Puerco) --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index ae7f0716a5..c13c75ff46 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/go-playground/validator/v10 v10.19.0 github.com/goccy/go-json v0.10.2 github.com/golang-migrate/migrate/v4 v4.17.0 + github.com/google/go-cmp v0.6.0 github.com/google/go-containerregistry v0.19.1 github.com/google/go-github/v56 v56.0.0 github.com/google/uuid v1.6.0 @@ -94,7 +95,6 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/google/cel-go v0.20.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/gorilla/schema v1.2.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/hashicorp/go-sockaddr v1.0.5 // indirect