1
0
mirror of https://github.com/strongdm/comply synced 2025-12-06 14:24:12 +00:00

Initial commit

This commit is contained in:
Justin McCarthy
2018-05-15 14:13:11 -07:00
commit 1746fe3ee7
1044 changed files with 265229 additions and 0 deletions

177
internal/cli/app.go Normal file
View File

@@ -0,0 +1,177 @@
package cli
import (
"context"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/strongdm/comply/internal/config"
"github.com/strongdm/comply/internal/plugin/github"
"github.com/urfave/cli"
)
// Version is set by the build system.
var Version = ""
// 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"
if Version == "" {
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 {
if c.Before == nil {
c.Before = beforeAll(bf...)
} else {
c.Before = beforeAll(append(bf, c.Before)...)
}
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 ticketingMustBeConfigured(c *cli.Context) error {
p := config.Config()
if p.Tickets == nil || len(p.Tickets) != 1 {
return feedbackError("comply.yml must contain a valid ticketing configuration")
}
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
}
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 * 5)
select {
case <-longishPull:
fmt.Print("Pulling strongdm/pandoc:latest Docker image (this will take some time) ")
go func() {
for {
fmt.Print(".")
select {
case <-done:
fmt.Print(" done.\n")
return
default:
time.Sleep(1 * time.Second)
}
}
}()
case <-done:
// in this case, the docker pull was quick -- suggesting we already have the container
}
}()
r, err := cli.ImagePull(ctx, "strongdm/pandoc:latest", types.ImagePullOptions{})
if err != nil {
return dockerErr
}
defer r.Close()
// hold function open until all docker IO is complete
io.Copy(ioutil.Discard, r)
return nil
}
func cleanContainers(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
}
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{All: true})
if err != nil {
return errors.Wrap(err, "error listing containers during cleanup")
}
for _, c := range containers {
// assume this container was leftover from previous aborted run
if strings.HasPrefix(c.Image, "strongdm/pandoc") {
d := time.Second * 2
err = cli.ContainerStop(ctx, c.ID, &d)
if err != nil {
fmt.Printf("Unable to stop container ID %s\n", c.ID)
}
err = cli.ContainerRemove(ctx, c.ID, types.ContainerRemoveOptions{Force: true})
if err != nil {
fmt.Printf("Unable to remove container ID %s, please attempt manual removal\n", c.ID)
}
}
}
return nil
}

23
internal/cli/build.go Normal file
View File

@@ -0,0 +1,23 @@
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",
Action: buildAction,
Before: beforeAll(dockerMustExist, cleanContainers),
}
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
View File

@@ -0,0 +1,4 @@
/*
Package cli defines comply commands and arguments.
*/
package cli

184
internal/cli/init.go Normal file
View File

@@ -0,0 +1,184 @@
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"
)
const whatNow = `Next steps:
* Customize this directory using a text editor ('cat TODO.md' for ideas)
* Try 'comply build' and 'comply serve'
* View output/index.html
* Add this directory to source control
`
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 (no spaces, no trailing separator)",
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")
}
fmt.Printf("\nComply relies on your ticketing system for optional procedure tracking. You can always come back and enable this integration later.\n\n\n")
chooser = promptui.Select{
Label: "Ticket System",
Items: []string{"GitHub", "Jira", "None"},
}
choice, _, err = chooser.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return err
}
var tickets map[string]interface{}
ticketing := model.GitHub
switch choice {
case 0:
ticketing = model.GitHub
case 1:
fmt.Println("\nHello Jira user! The Jira ticketing plugin is currently in development, please join us on Slack for a status update.")
ticketing = model.NoTickets
default:
ticketing = model.NoTickets
}
if ticketing != model.NoTickets {
chooser = promptui.Select{
Label: "Configure ticketing system?",
Items: []string{fmt.Sprintf("Configure %s now", string(ticketing)), "Configure later (via comply.yml)"},
}
choice, _, err = chooser.Run()
if err != nil {
fmt.Printf("Prompt failed %v\n", err)
return err
}
tickets = make(map[string]interface{})
ticketConfig := make(map[string]string)
tickets[string(ticketing)] = ticketConfig
if choice == 0 {
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 = tickets
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)
}
replace := make(map[string]string)
replace["Name"] = p.Name
err = theme.SaveTo(themeName, replace, config.ProjectRoot())
if err != nil {
return cli.NewExitError(err, 1)
}
success := fmt.Sprintf("%s Compliance initialized successfully!", name)
fmt.Printf("%s %s\n\n", promptui.IconGood, success)
fmt.Println(whatNow)
return nil
}

21
internal/cli/scheduler.go Normal file
View 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: beforeAll(projectMustExist, ticketingMustBeConfigured),
}
func schedulerAction(c *cli.Context) error {
err := syncAction(c)
if err != nil {
return err
}
return ticket.TriggerScheduled()
}

22
internal/cli/serve.go Normal file
View File

@@ -0,0 +1,22 @@
package cli
import (
"github.com/pkg/errors"
"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, cleanContainers),
}
func serveAction(c *cli.Context) error {
err := render.Build("output", true)
if err != nil {
return errors.Wrap(err, "serve failed")
}
return nil
}

29
internal/cli/sync.go Normal file
View File

@@ -0,0 +1,29 @@
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: beforeAll(projectMustExist, ticketingMustBeConfigured),
}
func syncAction(c *cli.Context) error {
// TODO: unhardcode plugin
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
View 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
}