2018-05-15 21:13:11 +00:00
package cli
import (
"context"
"fmt"
"io"
"io/ioutil"
"log"
2018-06-26 00:50:07 +00:00
"math/rand"
"net/http"
2018-05-15 21:13:11 +00:00
"os"
2018-05-23 21:15:39 +00:00
"os/exec"
2018-05-15 21:13:11 +00:00
"path/filepath"
2018-05-23 21:15:39 +00:00
"regexp"
"strconv"
2018-05-15 21:13:11 +00:00
"strings"
"time"
2018-06-26 00:50:07 +00:00
"unicode"
"unicode/utf8"
2018-05-15 21:13:11 +00:00
"github.com/docker/docker/api/types"
"github.com/docker/docker/client"
"github.com/pkg/errors"
"github.com/strongdm/comply/internal/config"
2018-07-21 00:45:20 +00:00
"github.com/strongdm/comply/internal/gitlab"
2018-08-29 22:48:07 +00:00
"github.com/strongdm/comply/internal/jira"
2018-05-15 21:13:11 +00:00
"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 {
2018-06-26 00:50:07 +00:00
beforeCommand ( initCommand , notifyVersion ) ,
2018-05-15 21:13:11 +00:00
}
2018-06-26 00:50:07 +00:00
app . Commands = append ( app . Commands , beforeCommand ( buildCommand , projectMustExist , notifyVersion ) )
app . Commands = append ( app . Commands , beforeCommand ( procedureCommand , projectMustExist , notifyVersion ) )
app . Commands = append ( app . Commands , beforeCommand ( schedulerCommand , projectMustExist , notifyVersion ) )
app . Commands = append ( app . Commands , beforeCommand ( serveCommand , projectMustExist , notifyVersion ) )
app . Commands = append ( app . Commands , beforeCommand ( syncCommand , projectMustExist , notifyVersion ) )
app . Commands = append ( app . Commands , beforeCommand ( todoCommand , projectMustExist , notifyVersion ) )
2018-05-15 21:13:11 +00:00
// Plugins
github . Register ( )
2018-05-30 23:28:31 +00:00
jira . Register ( )
2018-07-21 00:45:20 +00:00
gitlab . Register ( )
2018-05-15 21:13:11 +00:00
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
2018-05-23 21:15:39 +00:00
}
2018-06-26 00:50:07 +00:00
// notifyVersion asynchronously notifies the availability of version updates
func notifyVersion ( c * cli . Context ) error {
go func ( ) {
defer func ( ) {
recover ( ) // suppress panic
} ( )
r , err := http . Get ( "http://comply-releases.s3.amazonaws.com/channel/stable/VERSION" )
body , err := ioutil . ReadAll ( r . Body )
if err != nil {
// fail silently
}
version := strings . TrimSpace ( string ( body ) )
// only when numeric versions are present
firstRune , _ := utf8 . DecodeRuneInString ( string ( body ) )
if unicode . IsDigit ( firstRune ) && version != Version {
// only once every ~10 times
if rand . Intn ( 10 ) == 0 {
fmt . Fprintf ( os . Stderr , "a new version of comply is available" )
}
}
} ( )
return nil
}
2018-05-23 21:15:39 +00:00
func pandocMustExist ( c * cli . Context ) error {
2020-05-04 15:55:32 +00:00
eitherMustExistErr := fmt . Errorf ( "\n\nPlease install either Docker or the pandoc package and re-run `%s`. Find OS-specific pandoc installation instructions at: https://pandoc.org/installing.html" , c . Command . Name )
2018-05-23 21:15:39 +00:00
2021-10-19 19:05:25 +00:00
pandocBinaryExistErr , found , goodVersion , pdfLatex := pandocBinaryMustExist ( c )
2018-07-21 00:44:28 +00:00
dockerExistErr , inPath , isRunning := dockerMustExist ( c )
2018-06-02 00:01:22 +00:00
2021-10-19 19:05:25 +00:00
config . SetPandoc ( pandocBinaryExistErr == nil , dockerExistErr == nil )
2018-07-21 00:44:28 +00:00
check := func ( b bool ) string {
if b {
return "✔"
} else {
return "✖"
}
}
2018-05-23 23:48:35 +00:00
2021-10-19 19:05:25 +00:00
if pandocBinaryExistErr != nil && dockerExistErr != nil {
2018-07-21 00:44:28 +00:00
fmt . Printf ( `
[ % s ] pandoc binary installed and in PATH
[ % s ] pandoc version compatible
[ % s ] pdflatex binary installed and in PATH
[ % s ] docker binary installed
[ % s ] docker running
` , check ( found ) , check ( goodVersion ) , check ( pdfLatex ) , check ( inPath ) , check ( isRunning ) )
2018-05-23 23:48:35 +00:00
return eitherMustExistErr
2018-05-23 21:15:39 +00:00
}
2018-05-23 23:48:35 +00:00
2018-06-02 00:01:22 +00:00
// if we don't have pandoc, but we do have docker, execute a pull
2021-10-19 19:05:25 +00:00
if ! pandocImageExists ( context . Background ( ) ) && ( ( pandocBinaryExistErr != nil && dockerExistErr == nil ) || config . WhichPandoc ( ) == config . UseDocker ) {
canPullPandoc := strings . TrimSpace ( strings . ToLower ( os . Getenv ( "COMPLY_USE_LOCAL_PANDOC" ) ) ) != "true"
if canPullPandoc {
fmt . Println ( "Pulling docker image" )
dockerPull ( c )
} else {
return fmt . Errorf ( "Local Pandoc not found. Please set COMPLY_USE_LOCAL_PANDOC to false" )
}
2018-06-02 00:01:22 +00:00
}
2018-05-23 21:15:39 +00:00
return nil
}
2021-10-19 19:05:25 +00:00
var pandocBinaryMustExist = func ( c * cli . Context ) ( e error , found , goodVersion , pdfLatex bool ) {
2018-05-23 21:15:39 +00:00
cmd := exec . Command ( "pandoc" , "-v" )
outputRaw , err := cmd . Output ( )
2018-07-21 00:44:28 +00:00
e = nil
found = false
goodVersion = false
pdfLatex = false
2018-05-23 21:15:39 +00:00
if err != nil {
2018-07-21 00:44:28 +00:00
e = errors . Wrap ( err , "error calling pandoc" )
} else {
found = true
goodVersion = true
output := strings . TrimSpace ( ( string ( outputRaw ) ) )
versionErr := errors . New ( "cannot determine pandoc version" )
if ! strings . HasPrefix ( output , "pandoc" ) {
e = versionErr
goodVersion = false
} else {
re := regexp . MustCompile ( ` pandoc (\d+)\.(\d+) ` )
result := re . FindStringSubmatch ( output )
if len ( result ) != 3 {
e = versionErr
goodVersion = false
} else {
major , err := strconv . Atoi ( result [ 1 ] )
if err != nil {
e = versionErr
goodVersion = false
}
minor , err := strconv . Atoi ( result [ 2 ] )
if err != nil {
e = versionErr
goodVersion = false
}
if major < 2 || minor < 1 {
e = errors . New ( "pandoc 2.1 or greater required" )
goodVersion = false
}
}
}
2018-05-23 21:15:39 +00:00
}
2018-05-28 21:46:35 +00:00
// pdflatex must also be present
2018-07-21 00:08:09 +00:00
cmd = exec . Command ( "pdflatex" , "--version" )
2018-05-28 21:46:35 +00:00
outputRaw , err = cmd . Output ( )
if err != nil {
2018-07-21 00:44:28 +00:00
e = errors . Wrap ( err , "error calling pdflatex" )
} else if ! strings . Contains ( string ( outputRaw ) , "TeX" ) {
e = errors . New ( "pdflatex is required" )
} else {
pdfLatex = true
2018-05-28 21:46:35 +00:00
}
2018-07-21 00:44:28 +00:00
return e , found , goodVersion , pdfLatex
2018-05-15 21:13:11 +00:00
}
2021-10-19 19:05:25 +00:00
var dockerMustExist = func ( c * cli . Context ) ( e error , inPath , isRunning bool ) {
2018-06-02 00:01:22 +00:00
dockerErr := fmt . Errorf ( "Docker must be available in order to run `%s`" , c . Command . Name )
2018-07-21 00:44:28 +00:00
inPath = true
cmd := exec . Command ( "docker" , "--version" )
_ , err := cmd . Output ( )
if err != nil {
inPath = false
}
isRunning = true
2018-06-02 00:01:22 +00:00
ctx := context . Background ( )
cli , err := client . NewEnvClient ( )
if err != nil {
2018-07-21 00:44:28 +00:00
isRunning = false
return dockerErr , inPath , isRunning
2018-06-02 00:01:22 +00:00
}
_ , err = cli . Ping ( ctx )
if err != nil {
2018-07-21 00:44:28 +00:00
isRunning = false
return dockerErr , inPath , isRunning
2018-06-02 00:01:22 +00:00
}
2018-07-21 00:44:28 +00:00
return nil , inPath , isRunning
2018-06-02 00:01:22 +00:00
}
2021-10-19 19:05:25 +00:00
var pandocImageExists = func ( ctx context . Context ) bool {
cli , err := client . NewEnvClient ( )
if err != nil {
return false
}
options := types . ImageListOptions { All : true }
imageList , err := cli . ImageList ( ctx , options )
if err != nil {
return false
}
for _ , image := range imageList {
2021-11-03 11:47:41 +00:00
if len ( image . RepoTags ) > 0 && strings . Contains ( image . RepoTags [ 0 ] , "strongdm/pandoc:edge" ) {
2021-10-19 19:05:25 +00:00
return true
}
}
return false
}
var dockerPull = func ( c * cli . Context ) error {
2018-05-15 21:13:11 +00:00
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 :
2021-10-19 19:05:25 +00:00
fmt . Print ( "Pulling strongdm/pandoc:edge Docker image (this will take some time) " )
2018-05-15 21:13:11 +00:00
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
}
} ( )
2021-10-19 19:05:25 +00:00
r , err := cli . ImagePull ( ctx , "strongdm/pandoc:edge" , types . ImagePullOptions { } )
2018-05-15 21:13:11 +00:00
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 {
ctx := context . Background ( )
cli , err := client . NewEnvClient ( )
if err != nil {
2018-05-23 23:48:35 +00:00
// no Docker? nothing to clean.
return nil
}
_ , err = cli . Ping ( ctx )
if err != nil {
// no Docker? nothing to clean.
return nil
2018-05-15 21:13:11 +00:00
}
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
2021-11-01 18:04:27 +00:00
if strings . HasPrefix ( c . Image , "strongdm/pandoc:edge" ) {
2018-05-15 21:13:11 +00:00
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
}