1
0
mirror of https://github.com/strongdm/comply synced 2025-12-06 06:14:09 +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

6
internal/model/audit.go Normal file
View File

@@ -0,0 +1,6 @@
package model
type Audit struct {
ID string
Name string
}

36
internal/model/db.go Normal file
View File

@@ -0,0 +1,36 @@
package model
import (
"os"
"path/filepath"
"sync"
"github.com/nanobox-io/golang-scribble"
"github.com/strongdm/comply/internal/config"
)
var dbSingletonOnce sync.Once
var dbSingleton *scribble.Driver
// DB provides a singleton reference to a local json cache; will panic if storage location is not writeable.
func DB() *scribble.Driver {
dbSingletonOnce.Do(func() {
if _, err := os.Stat(filepath.Join(config.ProjectRoot(), ".comply", "cache")); os.IsNotExist(err) {
err = os.Mkdir(filepath.Join(config.ProjectRoot(), ".comply"), os.FileMode(0755))
if err != nil {
panic("could not create directory .comply: " + err.Error())
}
err = os.Mkdir(filepath.Join(config.ProjectRoot(), ".comply", "cache"), os.FileMode(0755))
if err != nil {
panic("could not create directory .comply/cache: " + err.Error())
}
}
db, err := scribble.New(filepath.Join(config.ProjectRoot(), ".comply", "cache"), nil)
if err != nil {
panic("unable to load comply data: " + err.Error())
}
dbSingleton = db
})
return dbSingleton
}

35
internal/model/db_test.go Normal file
View File

@@ -0,0 +1,35 @@
package model
import (
"os"
"path/filepath"
"testing"
"github.com/strongdm/comply/internal/config"
)
func TestSaveGet(t *testing.T) {
dir := os.TempDir()
config.SetProjectRoot(dir)
f, err := os.Create(filepath.Join(dir, "config.yml"))
if err != nil {
panic(err)
}
f.Close()
name := "Do something excellent"
err = DB().Write("tickets", "100", &Ticket{ID: "100", Name: name})
if err != nil {
panic(err)
}
ticket := &Ticket{}
err = DB().Read("tickets", "100", ticket)
if err != nil {
panic(err)
}
if ticket.Name != name {
t.Error("failed to read ticket")
}
}

14
internal/model/doc.go Normal file
View File

@@ -0,0 +1,14 @@
/*
Package model defines the comply data model.
Markdown Wrappers
The model package treats typed markdown files as model objects. All wrapped markdown documents are assumed to have a YAML header and a markdown body separated by three dashes: "---".
Local Ticket Cache
Tickets are defined externally (in the configured ticketing system), and cached locally for rapid dashboard rendering.
*/
package model

188
internal/model/fs.go Normal file
View File

@@ -0,0 +1,188 @@
package model
import (
"encoding/json"
"fmt"
"io/ioutil"
"strings"
"github.com/pkg/errors"
"github.com/strongdm/comply/internal/config"
"github.com/strongdm/comply/internal/path"
"gopkg.in/yaml.v2"
)
// ReadData loads all records from both the filesystem and ticket cache.
func ReadData() (*Data, error) {
tickets, err := ReadTickets()
if err != nil {
return nil, err
}
narratives, err := ReadNarratives()
if err != nil {
return nil, err
}
policies, err := ReadPolicies()
if err != nil {
return nil, err
}
procedures, err := ReadProcedures()
if err != nil {
return nil, err
}
standards, err := ReadStandards()
if err != nil {
return nil, err
}
return &Data{
Tickets: tickets,
Narratives: narratives,
Policies: policies,
Procedures: procedures,
Standards: standards,
}, nil
}
// ReadTickets returns all known tickets, or an empty list in the event the ticket cache is empty or unavailable.
func ReadTickets() ([]*Ticket, error) {
rt, err := DB().ReadAll("tickets")
if err != nil {
// empty list
return []*Ticket{}, nil
}
return tickets(rt)
}
func tickets(rawTickets []string) ([]*Ticket, error) {
var tickets []*Ticket
for _, rt := range rawTickets {
t := &Ticket{}
err := json.Unmarshal([]byte(rt), t)
if err != nil {
return nil, errors.Wrap(err, "malformed ticket JSON")
}
tickets = append(tickets, t)
}
return tickets, nil
}
// ReadStandards loads standard definitions from the filesystem.
func ReadStandards() ([]*Standard, error) {
var standards []*Standard
files, err := path.Standards()
if err != nil {
return nil, errors.Wrap(err, "unable to enumerate paths")
}
for _, f := range files {
s := &Standard{}
sBytes, err := ioutil.ReadFile(f.FullPath)
if err != nil {
return nil, errors.Wrap(err, "unable to read "+f.FullPath)
}
yaml.Unmarshal(sBytes, &s)
standards = append(standards, s)
}
return standards, nil
}
// ReadNarratives loads narrative descriptions from the filesystem.
func ReadNarratives() ([]*Narrative, error) {
var narratives []*Narrative
files, err := path.Narratives()
if err != nil {
return nil, errors.Wrap(err, "unable to enumerate paths")
}
for _, f := range files {
n := &Narrative{}
mdmd := loadMDMD(f.FullPath)
err = yaml.Unmarshal([]byte(mdmd.yaml), &n)
if err != nil {
return nil, errors.Wrap(err, "unable to parse "+f.FullPath)
}
n.Body = mdmd.body
n.FullPath = f.FullPath
n.ModifiedAt = f.Info.ModTime()
n.OutputFilename = fmt.Sprintf("%s-%s.pdf", config.Config().FilePrefix, n.Acronym)
narratives = append(narratives, n)
}
return narratives, nil
}
// ReadProcedures loads procedure descriptions from the filesystem.
func ReadProcedures() ([]*Procedure, error) {
var procedures []*Procedure
files, err := path.Procedures()
if err != nil {
return nil, errors.Wrap(err, "unable to enumerate paths")
}
for _, f := range files {
p := &Procedure{}
mdmd := loadMDMD(f.FullPath)
err = yaml.Unmarshal([]byte(mdmd.yaml), &p)
if err != nil {
return nil, errors.Wrap(err, "unable to parse "+f.FullPath)
}
p.Body = mdmd.body
p.FullPath = f.FullPath
p.ModifiedAt = f.Info.ModTime()
procedures = append(procedures, p)
}
return procedures, nil
}
// ReadPolicies loads policy documents from the filesystem.
func ReadPolicies() ([]*Policy, error) {
var policies []*Policy
files, err := path.Policies()
if err != nil {
return nil, errors.Wrap(err, "unable to enumerate paths")
}
for _, f := range files {
p := &Policy{}
mdmd := loadMDMD(f.FullPath)
err = yaml.Unmarshal([]byte(mdmd.yaml), &p)
if err != nil {
return nil, errors.Wrap(err, "unable to parse "+f.FullPath)
}
p.Body = mdmd.body
p.FullPath = f.FullPath
p.ModifiedAt = f.Info.ModTime()
p.OutputFilename = fmt.Sprintf("%s-%s.pdf", config.Config().FilePrefix, p.Acronym)
policies = append(policies, p)
}
return policies, nil
}
type metadataMarkdown struct {
yaml string
body string
}
func loadMDMD(path string) metadataMarkdown {
bytes, err := ioutil.ReadFile(path)
if err != nil {
panic(err)
}
content := string(bytes)
components := strings.Split(content, "---")
if len(components) == 1 {
panic(fmt.Sprintf("Malformed metadata markdown in %s, must be of the form: YAML\\n---\\nmarkdown content", path))
}
yaml := components[0]
body := strings.Join(components[1:], "---")
return metadataMarkdown{yaml, body}
}

17
internal/model/model.go Normal file
View File

@@ -0,0 +1,17 @@
package model
type Data struct {
Standards []*Standard
Narratives []*Narrative
Policies []*Policy
Procedures []*Procedure
Tickets []*Ticket
Audits []*Audit
}
type Revision struct {
Date string `yaml:"date"`
Comment string `yaml:"comment"`
}
type Satisfaction map[string][]string

View File

@@ -0,0 +1,44 @@
package model
import (
"encoding/json"
"strings"
"testing"
)
func TestMarshal(t *testing.T) {
d := Data{
Tickets: []*Ticket{
&Ticket{
ID: "t1",
},
},
Audits: []*Audit{
&Audit{
ID: "a1",
},
},
Procedures: []*Procedure{
&Procedure{
Code: "pro1",
},
},
Policies: []*Policy{
&Policy{
Name: "pol1",
},
},
}
m, _ := json.Marshal(d)
encoded := string(m)
if !strings.Contains(encoded, "t1") ||
!strings.Contains(encoded, "a1") ||
!strings.Contains(encoded, "pro1") ||
!strings.Contains(encoded, "pol1") {
t.Error("identifier not found in marshalled string")
}
}

View File

@@ -0,0 +1,15 @@
package model
import "time"
type Narrative struct {
Name string `yaml:"name"`
Acronym string `yaml:"acronym"`
Revisions []Revision `yaml:"majorRevisions"`
Satisfies Satisfaction `yaml:"satisfies"`
FullPath string
OutputFilename string
ModifiedAt time.Time
Body string
}

102
internal/model/plugin.go Normal file
View File

@@ -0,0 +1,102 @@
package model
import (
"fmt"
"sync"
"github.com/davecgh/go-spew/spew"
"github.com/strongdm/comply/internal/config"
)
var tsPluginsMu sync.Mutex
var tsPlugins = make(map[TicketSystem]TicketPlugin)
var tsConfigureOnce sync.Once
// TicketSystem is the type of ticket database.
type TicketSystem string
const (
// Jira from Atlassian.
Jira = TicketSystem("jira")
// GitHub from GitHub.
GitHub = TicketSystem("github")
// NoTickets indicates no ticketing system integration.
NoTickets = TicketSystem("none")
)
type TicketLinks struct {
ProcedureOpen string
ProcedureAll string
AuditOpen string
AuditAll string
}
// TicketPlugin models support for ticketing systems.
type TicketPlugin interface {
Get(ID string) (*Ticket, error)
FindOpen() ([]*Ticket, error)
FindByTag(name, value string) ([]*Ticket, error)
FindByTagName(name string) ([]*Ticket, error)
Create(ticket *Ticket, labels []string) error
Configure(map[string]interface{}) error
Prompts() map[string]string
Links() TicketLinks
LinkFor(ticket *Ticket) string
Configured() bool
}
// GetPlugin loads the ticketing database.
func GetPlugin(ts TicketSystem) TicketPlugin {
tsPluginsMu.Lock()
defer tsPluginsMu.Unlock()
tp, ok := tsPlugins[ts]
if !ok {
panic("Unknown ticket system: " + ts)
}
if config.Exists() {
tsConfigureOnce.Do(func() {
ticketsMap := config.Config().Tickets
hasTickets := true
cfg, ok := ticketsMap[string(ts)]
if !ok {
hasTickets = false
}
if hasTickets {
cfgTyped, ok := cfg.(map[interface{}]interface{})
if !ok {
spew.Dump(cfg)
panic(fmt.Sprintf("malformatted ticket configuration block `%s` in project YAML", string(ts)))
}
cfgStringed := make(map[string]interface{})
for k, v := range cfgTyped {
kS, ok := k.(string)
if !ok {
spew.Dump(cfgStringed)
panic(fmt.Sprintf("malformatted key in configuration block `%s` in project YAML", string(ts)))
}
cfgStringed[kS] = v
}
tp.Configure(cfgStringed)
}
})
}
return tp
}
// Register ticketing system plugin.
func Register(ts TicketSystem, plugin TicketPlugin) {
tsPluginsMu.Lock()
defer tsPluginsMu.Unlock()
_, ok := tsPlugins[ts]
if ok {
panic("Duplicate ticketing system registration: " + ts)
}
tsPlugins[ts] = plugin
}

15
internal/model/policy.go Normal file
View File

@@ -0,0 +1,15 @@
package model
import "time"
type Policy struct {
Name string `yaml:"name"`
Acronym string `yaml:"acronym"`
Revisions []Revision `yaml:"majorRevisions"`
Satisfies Satisfaction `yaml:"satisfies"`
FullPath string
OutputFilename string
ModifiedAt time.Time
Body string
}

View File

@@ -0,0 +1,16 @@
package model
import "time"
type Procedure struct {
Name string `yaml:"name"`
ID string `yaml:"id"`
Cron string `yaml:"cron"`
Revisions []Revision `yaml:"majorRevisions"`
Satisfies Satisfaction `yaml:"satisfies"`
FullPath string
OutputFilename string
ModifiedAt time.Time
Body string
}

View File

@@ -0,0 +1,49 @@
package model
type Control struct {
Family string `yaml:"family"`
Name string `yaml:"name"`
Description string `yaml:"description"`
}
type Standard struct {
Name string `yaml:"name"`
Controls map[string]Control `yaml:",inline"`
}
// ControlsSatisfied determines the unique controls currently satisfied by all Narratives, Policies, and Procedures
func ControlsSatisfied(data *Data) map[string][]string {
satisfied := make(map[string][]string)
appendSatisfaction := func(in map[string][]string, k string, v string) []string {
s, ok := in[k]
if !ok {
s = make([]string, 0)
}
s = append(s, v)
return s
}
for _, n := range data.Narratives {
for _, controlKeys := range n.Satisfies {
for _, key := range controlKeys {
satisfied[key] = appendSatisfaction(satisfied, key, n.OutputFilename)
}
}
}
for _, n := range data.Policies {
for _, controlKeys := range n.Satisfies {
for _, key := range controlKeys {
satisfied[key] = appendSatisfaction(satisfied, key, n.OutputFilename)
}
}
}
for _, n := range data.Procedures {
for _, controlKeys := range n.Satisfies {
for _, key := range controlKeys {
satisfied[key] = appendSatisfaction(satisfied, key, n.OutputFilename)
}
}
}
return satisfied
}

65
internal/model/ticket.go Normal file
View File

@@ -0,0 +1,65 @@
package model
import (
"strings"
"time"
)
type TicketState string
const (
Open = TicketState("open")
Closed = TicketState("closed")
)
type Ticket struct {
ID string
Name string
State TicketState
Body string
Attributes map[string]interface{}
ClosedAt *time.Time
CreatedAt *time.Time
UpdatedAt *time.Time
}
func (t *Ticket) ProcedureID() string {
md := t.metadata()
if v, ok := md["Procedure-ID"]; ok {
return v
}
return ""
}
func (t *Ticket) metadata() map[string]string {
md := make(map[string]string)
lines := strings.Split(t.Body, "\n")
for _, line := range lines {
// TODO: transition to RFC822 parsing
if strings.Contains(line, ":") {
tokens := strings.Split(line, ":")
if len(tokens) != 2 {
continue
}
md[strings.TrimSpace(tokens[0])] = strings.TrimSpace(tokens[1])
}
}
return md
}
func (t *Ticket) SetBool(name string) {
t.Attributes[name] = true
}
func (t *Ticket) Bool(name string) bool {
bi, ok := t.Attributes[name]
if !ok {
return false
}
b, ok := bi.(bool)
if !ok {
return false
}
return b
}