mirror of
https://github.com/strongdm/comply
synced 2025-12-06 14:24:12 +00:00
Initial commit
This commit is contained in:
116
internal/cli/app.go
Normal file
116
internal/cli/app.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/strongdm/comply/internal/config"
|
||||
"github.com/strongdm/comply/internal/plugin/github"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// Version is set by the build system.
|
||||
const Version = "0.0.0-development"
|
||||
|
||||
// Main should be invoked by the main function in the main package.
|
||||
func Main() {
|
||||
err := newApp().Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func newApp() *cli.App {
|
||||
app := cli.NewApp()
|
||||
app.Name = "comply"
|
||||
app.HideVersion = true
|
||||
app.Version = Version
|
||||
app.Usage = "policy compliance toolkit"
|
||||
|
||||
app.Commands = []cli.Command{
|
||||
initCommand,
|
||||
}
|
||||
|
||||
app.Commands = append(app.Commands, beforeCommand(buildCommand, projectMustExist))
|
||||
app.Commands = append(app.Commands, beforeCommand(schedulerCommand, projectMustExist))
|
||||
app.Commands = append(app.Commands, beforeCommand(serveCommand, projectMustExist))
|
||||
app.Commands = append(app.Commands, beforeCommand(syncCommand, projectMustExist))
|
||||
app.Commands = append(app.Commands, beforeCommand(todoCommand, projectMustExist))
|
||||
|
||||
// Plugins
|
||||
github.Register()
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
func beforeCommand(c cli.Command, bf ...cli.BeforeFunc) cli.Command {
|
||||
c.Before = beforeAll(bf...)
|
||||
return c
|
||||
}
|
||||
|
||||
func beforeAll(bf ...cli.BeforeFunc) cli.BeforeFunc {
|
||||
return func(c *cli.Context) error {
|
||||
for _, f := range bf {
|
||||
if err := f(c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func feedbackError(message string) error {
|
||||
return errors.New(fmt.Sprintf("\n\nERROR\n=====\n%s\n", message))
|
||||
}
|
||||
|
||||
func projectMustExist(c *cli.Context) error {
|
||||
_, err := ioutil.ReadFile(filepath.Join(config.ProjectRoot(), "comply.yml"))
|
||||
if err != nil {
|
||||
return feedbackError("command must be run from the root of a valid comply project (comply.yml must exist; have you run `comply init`?)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dockerMustExist(c *cli.Context) error {
|
||||
dockerErr := fmt.Errorf("Docker must be available in order to run `%s`", c.Command.Name)
|
||||
|
||||
ctx := context.Background()
|
||||
cli, err := client.NewEnvClient()
|
||||
if err != nil {
|
||||
return dockerErr
|
||||
}
|
||||
|
||||
r, err := cli.ImagePull(ctx, "strongdm/pandoc:latest", types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
return dockerErr
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
go func() {
|
||||
// if docker IO takes more than N seconds, notify user we're (likely) downloading the pandoc image
|
||||
longishPull := time.After(time.Second * 6)
|
||||
select {
|
||||
case <-longishPull:
|
||||
fmt.Println("Downloading strongdm/pandoc image (this may take sometime) ...")
|
||||
case <-done:
|
||||
// in this case, the docker pull was quick -- suggesting we already have the container
|
||||
}
|
||||
}()
|
||||
|
||||
// hold function open until all docker IO is complete
|
||||
io.Copy(ioutil.Discard, r)
|
||||
|
||||
return nil
|
||||
}
|
||||
29
internal/cli/build.go
Normal file
29
internal/cli/build.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/strongdm/comply/internal/render"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var buildCommand = cli.Command{
|
||||
Name: "build",
|
||||
ShortName: "b",
|
||||
Usage: "generate a static website summarizing the compliance program",
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "live, l",
|
||||
Usage: "rebuild static site after filesystem changes",
|
||||
},
|
||||
},
|
||||
Action: buildAction,
|
||||
Before: beforeAll(dockerMustExist),
|
||||
}
|
||||
|
||||
func buildAction(c *cli.Context) error {
|
||||
err := render.Build("output", false)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "build failed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
4
internal/cli/doc.go
Normal file
4
internal/cli/doc.go
Normal file
@@ -0,0 +1,4 @@
|
||||
/*
|
||||
Package cli defines comply commands and arguments.
|
||||
*/
|
||||
package cli
|
||||
153
internal/cli/init.go
Normal file
153
internal/cli/init.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/strongdm/comply/internal/config"
|
||||
"github.com/strongdm/comply/internal/model"
|
||||
"github.com/strongdm/comply/internal/theme"
|
||||
"github.com/urfave/cli"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var initCommand = cli.Command{
|
||||
Name: "init",
|
||||
Usage: "initialize a new compliance repository (interactive)",
|
||||
Action: initAction,
|
||||
}
|
||||
|
||||
func initAction(c *cli.Context) error {
|
||||
fi, _ := ioutil.ReadDir(config.ProjectRoot())
|
||||
if len(fi) > 0 {
|
||||
return errors.New("init must be run from an empty directory")
|
||||
}
|
||||
|
||||
atLeast := func(n int) func(string) error {
|
||||
return func(input string) error {
|
||||
if len(input) < n {
|
||||
return errors.New("Too short")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
noSpaces := func(s string) error {
|
||||
if strings.ContainsAny(s, "\n\t ") {
|
||||
return errors.New("Must not contain spaces")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Organization Name",
|
||||
Validate: atLeast(1),
|
||||
}
|
||||
|
||||
name, err := prompt.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
prompt = promptui.Prompt{
|
||||
Label: "PDF Filename Prefix",
|
||||
Default: strings.Split(name, " ")[0],
|
||||
Validate: noSpaces,
|
||||
}
|
||||
|
||||
prefix, err := prompt.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
chooser := promptui.Select{
|
||||
Label: "Compliance Templates",
|
||||
Items: []string{"SOC2", "Blank"},
|
||||
}
|
||||
|
||||
choice, _, err := chooser.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
themeName := "comply-blank"
|
||||
switch choice {
|
||||
case 0:
|
||||
themeName = "comply-soc2"
|
||||
case 1:
|
||||
themeName = "comply-blank"
|
||||
default:
|
||||
panic("unrecognized selection")
|
||||
}
|
||||
|
||||
chooser = promptui.Select{
|
||||
Label: "Ticket System",
|
||||
Items: []string{"Github", "JIRA"},
|
||||
}
|
||||
|
||||
choice, _, err = chooser.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return err
|
||||
}
|
||||
|
||||
ticketing := model.Github
|
||||
|
||||
switch choice {
|
||||
case 0:
|
||||
ticketing = model.Github
|
||||
case 1:
|
||||
ticketing = model.JIRA
|
||||
default:
|
||||
panic("unrecognized selection")
|
||||
}
|
||||
|
||||
ticketConfig := make(map[string]string)
|
||||
|
||||
plugin := model.GetPlugin(ticketing)
|
||||
ticketPrompts := plugin.Prompts()
|
||||
for k, prompt := range ticketPrompts {
|
||||
p := promptui.Prompt{
|
||||
Label: prompt,
|
||||
Validate: atLeast(2),
|
||||
}
|
||||
|
||||
v, err := p.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed: %v\n", err)
|
||||
return err
|
||||
}
|
||||
ticketConfig[k] = v
|
||||
}
|
||||
|
||||
p := config.Project{}
|
||||
p.Name = name
|
||||
p.FilePrefix = prefix
|
||||
p.Tickets = make(map[string]interface{})
|
||||
p.Tickets[string(ticketing)] = ticketConfig
|
||||
|
||||
x, _ := yaml.Marshal(&p)
|
||||
err = ioutil.WriteFile(filepath.Join(config.ProjectRoot(), "comply.yml"), x, os.FileMode(0644))
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
||||
err = theme.SaveTo(themeName, config.ProjectRoot())
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
||||
success := fmt.Sprintf("%s Compliance initialized successfully", name)
|
||||
fmt.Println(strings.Repeat("=", len(success)+2))
|
||||
fmt.Printf("%s %s\n", promptui.IconGood, success)
|
||||
|
||||
return nil
|
||||
}
|
||||
21
internal/cli/scheduler.go
Normal file
21
internal/cli/scheduler.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/strongdm/comply/internal/ticket"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var schedulerCommand = cli.Command{
|
||||
Name: "scheduler",
|
||||
Usage: "create tickets based on procedure schedule",
|
||||
Action: schedulerAction,
|
||||
Before: projectMustExist,
|
||||
}
|
||||
|
||||
func schedulerAction(c *cli.Context) error {
|
||||
err := syncAction(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ticket.TriggerScheduled()
|
||||
}
|
||||
21
internal/cli/serve.go
Normal file
21
internal/cli/serve.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/strongdm/comply/internal/render"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var serveCommand = cli.Command{
|
||||
Name: "serve",
|
||||
Usage: "live updating version of the build command",
|
||||
Action: serveAction,
|
||||
Before: beforeAll(dockerMustExist),
|
||||
}
|
||||
|
||||
func serveAction(c *cli.Context) error {
|
||||
err := render.Build("output", true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
28
internal/cli/sync.go
Normal file
28
internal/cli/sync.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/strongdm/comply/internal/model"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var syncCommand = cli.Command{
|
||||
Name: "sync",
|
||||
Usage: "sync ticket status to local cache",
|
||||
Action: syncAction,
|
||||
Before: projectMustExist,
|
||||
}
|
||||
|
||||
func syncAction(c *cli.Context) error {
|
||||
tp := model.GetPlugin(model.Github)
|
||||
tickets, err := tp.FindByTagName("comply")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, t := range tickets {
|
||||
err = model.DB().Write("tickets", t.ID, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
68
internal/cli/todo.go
Normal file
68
internal/cli/todo.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/strongdm/comply/internal/model"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var todoCommand = cli.Command{
|
||||
Name: "todo",
|
||||
Usage: "list declared vs satisfied compliance controls",
|
||||
Action: todoAction,
|
||||
Before: projectMustExist,
|
||||
}
|
||||
|
||||
func todoAction(c *cli.Context) error {
|
||||
d, err := model.ReadData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := tablewriter.NewWriter(os.Stdout)
|
||||
w.SetHeader([]string{"Standard", "Control", "Satisfied?", "Name"})
|
||||
|
||||
type row struct {
|
||||
standard string
|
||||
controlKey string
|
||||
satisfied string
|
||||
controlName string
|
||||
}
|
||||
|
||||
satisfied := model.ControlsSatisfied(d)
|
||||
|
||||
var rows []row
|
||||
for _, std := range d.Standards {
|
||||
for id, c := range std.Controls {
|
||||
sat := "NO"
|
||||
if _, ok := satisfied[id]; ok {
|
||||
sat = color.GreenString("YES")
|
||||
}
|
||||
|
||||
rows = append(rows, row{
|
||||
standard: std.Name,
|
||||
controlKey: id,
|
||||
satisfied: sat,
|
||||
controlName: c.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(rows, func(i, j int) bool {
|
||||
return rows[i].controlKey < rows[j].controlKey
|
||||
})
|
||||
|
||||
w.SetAutoWrapText(false)
|
||||
|
||||
for _, r := range rows {
|
||||
w.Append([]string{r.standard, r.controlKey, r.satisfied, r.controlName})
|
||||
}
|
||||
|
||||
w.Render()
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user