Skip to content

Commit

Permalink
Implement a quickstart command for minder (#1660)
Browse files Browse the repository at this point in the history
* Implement quickstart command for minder

Signed-off-by: Radoslav Dimitrov <[email protected]>

* Add enrolling to quickstart and also polish the prompt flow

Signed-off-by: Radoslav Dimitrov <[email protected]>

* Update README

Signed-off-by: Radoslav Dimitrov <[email protected]>

* Do not fail if profile exists

Signed-off-by: Radoslav Dimitrov <[email protected]>

* Do not rely on --provider for quickstart, default to github

Signed-off-by: Radoslav Dimitrov <[email protected]>

---------

Signed-off-by: Radoslav Dimitrov <[email protected]>
  • Loading branch information
rdimitrov authored Nov 16, 2023
1 parent 2445449 commit 453e582
Show file tree
Hide file tree
Showing 13 changed files with 682 additions and 224 deletions.
49 changes: 35 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,39 +83,60 @@ minder auth login

Upon completion, you should see that the Minder Server is set to `api.stacklok.com`.

## Enroll a repository provider
## Quickstart

Minder supports GitHub as a provider to enroll repositories. To enroll your provider, run:
Minder provides a "happy path" that guides you through the process of creating your first profile in Minder.
In just a few seconds, you will register your repositories and enable secret scanning protection for all of them.
To do so, run:

```bash
minder provider enroll --provider github
minder quickstart
```

A browser session will open, and you will be prompted to login to your GitHub.
Once you have granted Minder access, you will be redirected back, and the user will be enrolled.
The minder CLI application will report the session is complete.
This will prompt you to enroll your provider, select the repositories you'd like, create the `secret_scanning`
rule type and create a profile which enables secret scanning for the selected repositories.

## Register a repository

Now that you've granted the GitHub app permissions to access your repositories, you can register them:
To see the status of your profile, run:

```bash
minder repo register --provider github
minder profile_status list --profile quickstart-profile --detailed
```

Once you've registered the repositories, the Minder server will listen for events from GitHub and will
automatically create the necessary webhooks for you.
You should see the overall profile status and a detailed view of the rule evaluation statuses for each of your registered repositories.

Minder will continue to keep track of your repositories and will ensure to fix any drifts from the desired state by
using the `remediate` feature or alert you, if needed, using the `alert` feature.

Congratulations! 🎉 You've now successfully created your first profile!

## What's next?

You can now continue to explore Minder's features by adding or removing more repositories, create more profiles with
various rules, and much more. There's a lot more to Minder than just secret scanning.

Now you can run `minder` commands against the public instance of Minder where you can manage your registered repositories
and create custom profiles that would help ensure your repositories are configured consistently and securely.
The `secret_scanning` rule is just one of the many rule types that Minder supports.

You can see the full list of ready-to-use rules and profiles
maintained by Minder's team here - [stacklok/minder-rules-and-profiles](https://github.com/stacklok/minder-rules-and-profiles).

In case there's something you don't find there yet, Minder is designed to be extensible.
This allows for users to create their own custom rule types and profiles and ensure the specifics of their security
posture are attested to.

Now that you have everything set up, you can continue to run `minder` commands against the public instance of Minder
where you can manage your registered repositories, create profiles, rules and much more, so you can ensure your repositories are
configured consistently and securely.

For more information about `minder`, see:
* `minder` CLI commands - [Docs](https://minder-docs.stacklok.dev/ref/cli/minder).
* `minder` REST API Documentation - [Docs](https://minder-docs.stacklok.dev/ref/api).
* `minder` rules and profiles maintained by Minder's team - [GitHub](https://github.com/stacklok/minder-rules-and-profiles).
* Minder documentation - [Docs](https://minder-docs.stacklok.dev).

# Development

This section describes how to build and run Minder from source.

## Build from source

### Prerequisites
Expand Down
4 changes: 2 additions & 2 deletions cmd/cli/app/profile/profile_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ within a minder control plane.`,
return fmt.Errorf("error creating profile: %w", err)
}

table := initializeTable(cmd)
renderProfileTable(resp.GetProfile(), table)
table := InitializeTable(cmd)
RenderProfileTable(resp.GetProfile(), table)
table.Render()
return nil
},
Expand Down
4 changes: 2 additions & 2 deletions cmd/cli/app/profile/profile_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,9 @@ func init() {
}

func handleGetTableOutput(cmd *cobra.Command, profile *pb.Profile) {
table := initializeTable(cmd)
table := InitializeTable(cmd)

renderProfileTable(profile, table)
RenderProfileTable(profile, table)

table.Render()
}
4 changes: 2 additions & 2 deletions cmd/cli/app/profile/profile_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ func init() {
}

func handleListTableOutput(cmd *cobra.Command, resp *pb.ListProfilesResponse) {
table := initializeTable(cmd)
table := InitializeTable(cmd)

for _, v := range resp.Profiles {
renderProfileTable(v, table)
RenderProfileTable(v, table)
}
table.Render()
}
4 changes: 2 additions & 2 deletions cmd/cli/app/profile/profile_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ within a minder control plane.`,
return fmt.Errorf("error updating profile: %w", err)
}

table := initializeTable(cmd)
renderProfileTable(resp.GetProfile(), table)
table := InitializeTable(cmd)
RenderProfileTable(resp.GetProfile(), table)
table.Render()
return nil
},
Expand Down
6 changes: 4 additions & 2 deletions cmd/cli/app/profile/table_render.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import (
minderv1 "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1"
)

func initializeTable(cmd *cobra.Command) *tablewriter.Table {
// InitializeTable initializes the table for rendering profiles
func InitializeTable(cmd *cobra.Command) *tablewriter.Table {
table := tablewriter.NewWriter(cmd.OutOrStdout())
table.SetHeader([]string{"Id", "Name", "Provider", "Entity", "Rule", "Rule Params", "Rule Definition"})
table.SetRowLine(true)
Expand All @@ -36,7 +37,8 @@ func initializeTable(cmd *cobra.Command) *tablewriter.Table {
return table
}

func renderProfileTable(
// RenderProfileTable renders the profile table
func RenderProfileTable(
p *minderv1.Profile,
table *tablewriter.Table,
) {
Expand Down
147 changes: 81 additions & 66 deletions cmd/cli/app/provider/provider_enroll.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,86 +120,101 @@ actions such as adding repositories.`,
}
},
Run: func(cmd *cobra.Command, args []string) {
provider := util.GetConfigValue(viper.GetViper(), "provider", "provider", cmd, "").(string)
if provider != ghclient.Github {
fmt.Fprintf(os.Stderr, "Only %s is supported at this time\n", ghclient.Github)
os.Exit(1)
}
project := viper.GetString("project-id")
pat := util.GetConfigValue(viper.GetViper(), "token", "token", cmd, "").(string)
owner := util.GetConfigValue(viper.GetViper(), "owner", "owner", cmd, "").(string)

// Ask for confirmation if an owner is set on purpose
ownerPromptStr := "your personal account"
if owner != "" {
ownerPromptStr = fmt.Sprintf("the %s organisation", owner)
}
yes := cli.PrintYesNoPrompt(cmd,
fmt.Sprintf("You are about to enroll repositories from %s.", ownerPromptStr),
"Do you confirm?",
"Enroll operation cancelled.")
if !yes {
return
msg, err := EnrollProviderCmd(cmd, args)
util.ExitNicelyOnError(err, msg)
},
}

// EnrollProviderCmd is the command for enrolling a provider
func EnrollProviderCmd(cmd *cobra.Command, _ []string) (string, error) {
provider := util.GetConfigValue(viper.GetViper(), "provider", "provider", cmd, "").(string)
if provider != ghclient.Github {
msg := fmt.Sprintf("Only %s is supported at this time", ghclient.Github)
return "", fmt.Errorf(msg)
}
project := viper.GetString("project")
pat := util.GetConfigValue(viper.GetViper(), "token", "token", cmd, "").(string)
owner := util.GetConfigValue(viper.GetViper(), "owner", "owner", cmd, "").(string)

// Ask for confirmation if an owner is set on purpose
ownerPromptStr := "your personal account"
if owner != "" {
ownerPromptStr = fmt.Sprintf("the %s organisation", owner)
}
yes := cli.PrintYesNoPrompt(cmd,
fmt.Sprintf("You are about to enroll repositories from %s.", ownerPromptStr),
"Do you confirm?",
"Enroll operation cancelled.")
if !yes {
return "", nil
}

conn, err := util.GrpcForCommand(cmd, viper.GetViper())
if err != nil {
return "Error getting grpc connection", err
}
defer conn.Close()

client := pb.NewOAuthServiceClient(conn)
ctx, cancel := util.GetAppContext()
defer cancel()
oAuthCallbackCtx, oAuthCancel := context.WithTimeout(context.Background(), MAX_CALLS*time.Second)
defer oAuthCancel()

if pat != "" {
// use pat for enrollment
_, err := client.StoreProviderToken(context.Background(),
&pb.StoreProviderTokenRequest{Provider: provider, ProjectId: project, AccessToken: pat, Owner: &owner})
if err != nil {
return "Error storing token", err
}

conn, err := util.GrpcForCommand(cmd, viper.GetViper())
util.ExitNicelyOnError(err, "Error getting grpc connection")
defer conn.Close()
cli.PrintCmd(cmd, "Provider enrolled successfully")
return "", nil
}

client := pb.NewOAuthServiceClient(conn)
ctx, cancel := util.GetAppContext()
defer cancel()
oAuthCallbackCtx, oAuthCancel := context.WithTimeout(context.Background(), MAX_CALLS*time.Second)
defer oAuthCancel()
// Get random port
port, err := rand.GetRandomPort()
if err != nil {
return "Error getting random port", err
}

if pat != "" {
// use pat for enrollment
_, err := client.StoreProviderToken(context.Background(),
&pb.StoreProviderTokenRequest{Provider: provider, ProjectId: project, AccessToken: pat, Owner: &owner})
util.ExitNicelyOnError(err, "Error storing token")
resp, err := client.GetAuthorizationURL(ctx, &pb.GetAuthorizationURLRequest{
Provider: provider,
ProjectId: project,
Cli: true,
Port: int32(port),
Owner: &owner,
})
if err != nil {
return "Error getting authorization URL", err
}

cli.PrintCmd(cmd, "Provider enrolled successfully")
return
}
fmt.Printf("Your browser will now be opened to: %s\n", resp.GetUrl())
fmt.Println("Please follow the instructions on the page to complete the OAuth flow.")
fmt.Println("Once the flow is complete, the CLI will close")
fmt.Println("If this is a headless environment, please copy and paste the URL into a browser on a different machine.")

// Get random port
port, err := rand.GetRandomPort()
util.ExitNicelyOnError(err, "Error getting random port")

resp, err := client.GetAuthorizationURL(ctx, &pb.GetAuthorizationURLRequest{
Provider: provider,
ProjectId: project,
Cli: true,
Port: int32(port),
Owner: &owner,
})
util.ExitNicelyOnError(err, "Error getting authorization URL")

fmt.Printf("Your browser will now be opened to: %s\n", resp.GetUrl())
fmt.Println("Please follow the instructions on the page to complete the OAuth flow.")
fmt.Println("Once the flow is complete, the CLI will close")
fmt.Println("If this is a headless environment, please copy and paste the URL into a browser on a different machine.")

if err := browser.OpenURL(resp.GetUrl()); err != nil {
fmt.Fprintf(os.Stderr, "Error opening browser: %s\n", err)
fmt.Println("Please copy and paste the URL into a browser.")
}
openTime := time.Now().Unix()
if err := browser.OpenURL(resp.GetUrl()); err != nil {
fmt.Fprintf(os.Stderr, "Error opening browser: %s\n", err)
fmt.Println("Please copy and paste the URL into a browser.")
}
openTime := time.Now().Unix()

var wg sync.WaitGroup
wg.Add(1)
var wg sync.WaitGroup
wg.Add(1)

go callBackServer(oAuthCallbackCtx, provider, project, fmt.Sprintf("%d", port), &wg, client, openTime)
wg.Wait()
go callBackServer(oAuthCallbackCtx, provider, project, fmt.Sprintf("%d", port), &wg, client, openTime)
wg.Wait()

cli.PrintCmd(cmd, "Provider enrolled successfully")
},
cli.PrintCmd(cmd, "Provider enrolled successfully")
return "", nil
}

func init() {
ProviderCmd.AddCommand(enrollProviderCmd)
enrollProviderCmd.Flags().StringP("provider", "p", "", "Name for the provider to enroll")
enrollProviderCmd.Flags().StringP("project-id", "g", "", "ID of the project for enrolling the provider")
enrollProviderCmd.Flags().StringP("project", "r", "", "ID of the project for enrolling the provider")
enrollProviderCmd.Flags().StringP("token", "t", "", "Personal Access Token (PAT) to use for enrollment")
enrollProviderCmd.Flags().StringP("owner", "o", "", "Owner to filter on for provider resources")
if err := enrollProviderCmd.MarkFlagRequired("provider"); err != nil {
Expand Down
26 changes: 26 additions & 0 deletions cmd/cli/app/quickstart/embed/profile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Copyright 2023 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.
---
# sample profile for the quickstart command
version: v1
type: profile
name: quickstart-profile
context:
provider: github
alert: "on"
remediate: "on"
repository:
- type: secret_scanning
def:
enabled: true
Loading

0 comments on commit 453e582

Please sign in to comment.