diff --git a/pkg/cli/export/export.go b/pkg/cli/export/export.go new file mode 100644 index 00000000..ba343625 --- /dev/null +++ b/pkg/cli/export/export.go @@ -0,0 +1,85 @@ +package export + +import ( + "fmt" + "io" + + "github.com/opencontrol/compliance-masonry/pkg/cli/clierrors" + "github.com/opencontrol/compliance-masonry/tools/constants" + "github.com/spf13/cobra" +) + +// docxtemplater boolean variable +var docxtemplater bool + +// flatten boolean variable +var flattenFlag bool + +// keys boolen flag +var keysFlag bool + +// NewCmdGet gets all the compliance dependencies +func NewCmdExport(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "export", + Short: "Export to consolidated output", + Run: func(cmd *cobra.Command, args []string) { + err := RunExport(out, cmd, args) + clierrors.CheckError(err) + }, + } + cmd.Flags().StringP("opencontrol", "o", constants.DefaultDestination, "Set opencontrol directory") + cmd.Flags().StringP("dest", "d", constants.DefaultJSONFile, "Destination file for output") + cmd.Flags().BoolVarP(&flattenFlag, "flatten", "n", false, "Flatten results file") + cmd.Flags().StringP("format", "f", constants.DefaultOutputFormat, "Output format for destination file") + cmd.Flags().BoolVarP(&keysFlag, "keys", "k", false, "Keys to use when processing arrays while flattening") + cmd.Flags().BoolVarP(&docxtemplater, "docxtemplater", "x", false, "Use docxtemplater format") + cmd.Flags().StringP("separator", "s", constants.DefaultKeySeparator, "Separator to use when flattening keys") + return cmd +} + +func RunExport(out io.Writer, cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("certification type not specified") + } + + // read parms + parmOpencontrols := cmd.Flag("opencontrols").Value.String() + parmDestination := cmd.Flag("destination").Value.String() + parmOutputFormat := cmd.Flag("format").Value.String() + parmFlatten := false + parmInferKeys := false + parmDocxtemplater := false + parmKeySeparator := cmd.Flag("separator").Value.String() + + // convert to enum + outputFormat, err := ToOutputFormat(parmOutputFormat) + if err != nil { + return clierrors.NewExitError(err.Error(), 1) + } + + // --docxtemplater always forces --flatten + if parmDocxtemplater { + parmFlatten = true + } + + // construct args + config := Config{ + Certification: args[0], + OpencontrolDir: parmOpencontrols, + DestinationFile: parmDestination, + OutputFormat: outputFormat, + Flatten: parmFlatten, + InferKeys: parmInferKeys, + Docxtemplater: parmDocxtemplater, + KeySeparator: parmKeySeparator, + } + + // invoke command + errs := Export(config) + if errs != nil && len(errs) > 0 { + err := clierrors.NewMultiError(errs...) + return clierrors.NewExitError(err.Error(), 1) + } + return nil +} diff --git a/pkg/cli/export/exportFormat.go b/pkg/cli/export/exportFormat.go new file mode 100644 index 00000000..14dc06b3 --- /dev/null +++ b/pkg/cli/export/exportFormat.go @@ -0,0 +1,159 @@ +package export + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "gopkg.in/yaml.v2" + "io" + "log" + "os" + "strings" + + "github.com/opencontrol/compliance-masonry/pkg/lib" + "github.com/opencontrol/compliance-masonry/pkg/lib/common" + "github.com/opencontrol/compliance-masonry/tools/certifications" +) + +//////////////////////////////////////////////////////////////////////// +// Package functions + +// exportJSON - JSON output +func exportJSON(config *Config, workspace common.Workspace, output *exportOutput, writer io.Writer) []error { + // result + var errors []error + + // work vars + var byteSlice []byte + var err error + + // do the work + byteSlice, err = json.Marshal(output) + if err != nil { + return returnErrors(err) + } + + // flatten output? + if config.Flatten { + if config.Debug { + log.Println("JSON: Flatten") + } + + // decode json first + mapped := map[string]interface{}{} + err = json.Unmarshal(byteSlice, &mapped) + if err != nil { + return returnErrors(err) + } + + // flatten the JSON (recursive) + var flattened = make(map[string]interface{}) + var lkey string + err := flatten(config, mapped, lkey, &flattened) + if err != nil { + return returnErrors(err) + } + var flattenedByteSlice []byte + flattenedByteSlice, err = json.Marshal(flattened) + if err != nil { + return returnErrors(err) + } + writer.Write(flattenedByteSlice) + } else { + // direct output + writer.Write(byteSlice) + } + + return errors +} + +// exportYAML - YAML output +func exportYAML(config *Config, workspace common.Workspace, output *exportOutput, writer io.Writer) []error { + // result + var dummyErrors []error + + // work vars + var byteSlice []byte + var err error + + // do the work + byteSlice, err = yaml.Marshal(output) + if err != nil { + return returnErrors(err) + } + + // flatten output? + if config.Flatten { + // we do not support flatten for YAML - returns 'map[interface {}]interface {}' + return returnErrors(errors.New("--flatten unsupported for YAML")) + } + // direct output + writer.Write(byteSlice) + + // just so we can return an empty array (not nil) + return dummyErrors +} + +// internal - handle export +func export(config *Config, workspace common.Workspace) []error { + // sanity + if len(strings.TrimSpace(config.DestinationFile)) == 0 { + return returnErrors(errors.New("empty destination files")) + } + + // create our work object + var output exportOutput + output.Config = config + output.Data.Certification = workspace.GetCertification() + output.Data.Components = workspace.GetAllComponents() + output.Data.Standards = workspace.GetAllStandards() + + // handle output destination + var writer io.Writer + if config.DestinationFile == "-" { + // send to stdout + writer = os.Stdout + } else if config.DestinationFile == "-str-" { + // send to null buffer + var b bytes.Buffer + writer = bufio.NewWriter(&b) + } else { + // send to file + file, err := os.Create(config.DestinationFile) + if err != nil { + return returnErrors(err) + } + writer = file + defer file.Close() + } + + // handle the output + switch config.OutputFormat { + case FormatJSON: + return exportJSON(config, workspace, &output, writer) + case FormatYAML: + return exportYAML(config, workspace, &output, writer) + default: + return returnErrors(fmt.Errorf("unsupported OutputFormat '%s'", config.OutputFormat)) + } +} + +// Export loads the inventory and writes output to destinaation +func Export(config Config) []error { + // resolve the actual certification to use + certificationPath, errs := certifications.GetCertification(config.OpencontrolDir, config.Certification) + if errs != nil && len(errs) > 0 { + return errs + } + + // load all workspace data + workspace, errs := lib.LoadData(config.OpencontrolDir, certificationPath) + if errs != nil && len(errs) > 0 { + return errs + } + + // retrieve workspace data and write to output + return export(&config, workspace) +} diff --git a/pkg/cli/export/export_config.go b/pkg/cli/export/export_config.go new file mode 100644 index 00000000..ce5f2443 --- /dev/null +++ b/pkg/cli/export/export_config.go @@ -0,0 +1,115 @@ +package export + +import ( + "bytes" + "encoding/json" + + lib_certifications "github.com/opencontrol/compliance-masonry/pkg/lib/certifications" + "github.com/opencontrol/compliance-masonry/pkg/lib/common" +) + +//////////////////////////////////////////////////////////////////////// +// Package structures + +// Config contains settings for this object +type Config struct { + Debug bool + // remainder are configuration settings local to Export + Certification string + OpencontrolDir string + DestinationFile string + OutputFormat OutputFormat + Flatten bool + InferKeys bool + Docxtemplater bool + KeySeparator string +} + +// internal - structure for JSON / YAML output +type exportData struct { + Certification common.Certification + Components []common.Component + Standards []common.Standard +} + +// MarshalJSON provides JSON support +func (p *exportData) MarshalJSON() (b []byte, e error) { + // start the output + buffer := bytes.NewBufferString("{") + + // certification + buffer.WriteString("\"certification\":") + bytesJSON, err := lib_certifications.MarshalJSON(p.Certification) + if err != nil { + return nil, err + } + buffer.WriteString(string(bytesJSON)) + + // iterate over components + if len(p.Components) > 0 { + buffer.WriteString(",\"components\":[") + for i, v := range p.Components { + bytesJSON, err := json.Marshal(v) + if err != nil { + return nil, err + } + if i > 0 { + buffer.WriteString(",") + } + buffer.WriteString(string(bytesJSON)) + } + buffer.WriteString("]") + } + + // iterate over standards + if len(p.Standards) > 0 { + buffer.WriteString(",\"standards\":[") + for i, v := range p.Standards { + bytesJSON, err := json.Marshal(v) + if err != nil { + return nil, err + } + if i > 0 { + buffer.WriteString(",") + } + buffer.WriteString(string(bytesJSON)) + } + buffer.WriteString("]") + } + + // finish json + buffer.WriteString("}") + return buffer.Bytes(), nil +} + +// internal - structure for all exported data +type exportOutput struct { + Config *Config + Data exportData +} + +// MarshalJSON provides JSON support +func (p *exportOutput) MarshalJSON() (b []byte, e error) { + // start the output + buffer := bytes.NewBufferString("{") + + // config section + buffer.WriteString("\"config\":") + bytesConfig, err := json.Marshal(p.Config) + if err != nil { + return nil, err + } + buffer.WriteString(string(bytesConfig)) + + // data section + buffer.WriteString(",\"data\":") + bytesData, err := json.Marshal(&p.Data) + if err != nil { + return nil, err + } + buffer.WriteString(string(bytesData)) + + // close output + buffer.WriteString("}") + return buffer.Bytes(), nil +} diff --git a/pkg/cli/export/export_flatten.go b/pkg/cli/export/export_flatten.go new file mode 100644 index 00000000..8a12bb7d --- /dev/null +++ b/pkg/cli/export/export_flatten.go @@ -0,0 +1,340 @@ +package export + +import ( + "fmt" + "log" + "regexp" + "strings" +) + +//////////////////////////////////////////////////////////////////////// +// Package functions + +// flattenScalar - handle scalar flatten if possible +func flattenScalar(config *Config, value interface{}, key string, flattened *map[string]interface{}) bool { + // first, check all supported simple types + result := true + if _, okStr := value.(string); okStr { + if config.Debug { + log.Printf("flatten:Scalar(string): %s=%s\n", key, value.(string)) + } + (*flattened)[key] = value.(string) + } else if _, okFloat64 := value.(float64); okFloat64 { + if config.Debug { + log.Printf("flatten:Scalar(float64): %s=%f\n", key, value.(float64)) + } + (*flattened)[key] = value.(float64) + } else if _, okBool := value.(bool); okBool { + if config.Debug { + log.Printf("flatten:Scalar(bool): %s=%t\n", key, value.(bool)) + } + (*flattened)[key] = value.(bool) + } else { + result = false + } + if config.Debug { + debugHook(config, flattened) + } + return result +} + +// flattenArray - handle embedded arrays +func flattenArray(config *Config, value interface{}, key string, flattened *map[string]interface{}) (bool, error) { + // are we an array? + input, okArray := value.([]interface{}) + if !okArray { + return false, nil + } + if config.Debug { + log.Printf("flatten:Array:process %s\n", key) + } + + // use a target array as the flattened value for this element + var theArrayValue interface{} + var targetArray []interface{} + + // docxtemplater: embed iff all elements are scalar + embedArray := false + if config.Docxtemplater { + embedArray = true + for i := 0; i < len(input); i++ { + theArrayValue = input[i] + if !isScalar(theArrayValue) { + embedArray = false + break + } + } + if embedArray && config.Debug { + log.Printf("flatten:Array:embedArray %s\n", key) + } + } + + // iterate over the array + for i := 0; i < len(input); i++ { + // the value to flatten + theArrayValue = input[i] + + // what key / map will we use for flattening? + var arrayKeyToUse string + var flattenedToUse *map[string]interface{} + + // what should the target map be? + if embedArray { + // all scalar values mean we will use a simple map with a well-known data name + var docxtemplaterArrayMap = make(map[string]interface{}) + arrayKeyToUse = "data" + flattenedToUse = &docxtemplaterArrayMap + } else { + // handle the key name to use + lkey := key + config.KeySeparator + arrayKeyToUse = discoverKey(config, theArrayValue, lkey, i) + if config.Debug { + log.Printf("flatten:Array:discoverKey %s=%s\n", key, arrayKeyToUse) + } + flattenedToUse = flattened + } + + // call the standard flatten function + processed, err := flattenDriver(config, theArrayValue, arrayKeyToUse, flattenedToUse) + if err != nil { + return processed, err + } + if !processed { + return false, fmt.Errorf("key '%s[%d]': flattenDriver returns not processed for '%v'", key, i, theArrayValue) + } + if config.Debug { + debugHook(config, flattenedToUse) + } + + // docxtemplater: simple arrays are embedded (not flattened) + if embedArray { + // account for single elements with no key; use 'name' as the key to match docxtemplater + if len(*flattenedToUse) == 1 { + if val, mapHasEmptyKey := (*flattenedToUse)[""]; mapHasEmptyKey { + if config.Debug { + log.Printf("flatten:Array:embedArray:replaceEmptyKey %s\n", key) + } + (*flattenedToUse)["name"] = val + delete((*flattenedToUse), "") + } + } + targetArray = append(targetArray, *flattenedToUse) + if config.Debug { + debugHook(config, flattenedToUse) + } + } + } + + // if we are using docxtemplater format, append targetArray as single value for this key + if config.Docxtemplater && (targetArray != nil) { + if config.Debug { + debugHook(config, flattened) + log.Printf("flatten:Array:useTargetArray %s\n", key) + } + (*flattened)[key] = targetArray + if config.Debug { + debugHook(config, flattened) + } + } + + // all is well + if config.Debug { + debugHook(config, flattened) + } + return true, nil +} + +// flattenMap - handle dictionary +func flattenMap(config *Config, value interface{}, key string, flattened *map[string]interface{}) (bool, error) { + // must be a map type + input, okMapType := value.(map[string]interface{}) + if !okMapType { + return false, nil + } + if config.Debug { + log.Printf("flatten:Map:process %s\n", key) + } + + // iterate over key-value pairs + var newKey string + for rkey, subValue := range input { + // first-time logic + if key != "" { + newKey = key + config.KeySeparator + rkey + } else { + if config.Debug { + log.Printf("flatten:Map:isFirstTime %s\n", key) + } + newKey = rkey + } + + // check all of the known types + processed, err := flattenDriver(config, subValue, newKey, flattened) + if err != nil { + return processed, err + } + if !processed { + return false, fmt.Errorf("key '%s': flattenDriver returns not processed for '%v'", newKey, subValue) + } + } + + // all is well + if config.Debug { + debugHook(config, flattened) + } + return true, nil +} + +// flattenDriver - handle all known types for flattening +func flattenDriver(config *Config, value interface{}, key string, flattened *map[string]interface{}) (bool, error) { + // account for unset value - just ignore (?) + if value == nil { + if config.Debug { + log.Printf("flatten: No value for %s\n", key) + } + return true, nil + } + + // some variables + processed := false + var err error + + // scalar is simplest - does not invoke anything lower + processed = flattenScalar(config, value, key, flattened) + if processed { + return processed, nil + } + + // array can recurse; trap error + processed, err = flattenArray(config, value, key, flattened) + if err != nil { + return processed, err + } + if processed { + return processed, nil + } + + // map can recurse; trap error + processed, err = flattenMap(config, value, key, flattened) + if err != nil { + return processed, err + } + if processed { + return processed, nil + } + + // we have a truly unknown type + if config.Debug { + debugHook(config, flattened) + } + return false, fmt.Errorf("key '%s': unknown value '%v'", key, value) +} + +// flattenNormalize - called after everything else, handles control normalization +func flattenNormalize(config *Config, flattened *map[string]interface{}) error { + // discover all controls + var allControls []string + + // create the regex expressions we will use + regexControlKeyPattern := "^(?P(?Pdata" + config.KeySeparator + + "components" + config.KeySeparator + "(?P.*?))" + config.KeySeparator + + "satisfies" + config.KeySeparator + "(?P.*?)" + + config.KeySeparator + ")control_key$" + regexControlKeyExp, _ := regexp.Compile(regexControlKeyPattern) + + // component info we will want to extract for each normalized control + componentCheck := make(map[string]string) + componentCheck["key"] = "key" + componentCheck["responsible_role"] = "responsible_role" + + // iterate over flattened map + for key, value := range *flattened { + // must be a string + valueStr, okStr := value.(string) + if !okStr { + continue + } + + // anything to do? + regexControlKeyMatch := regexControlKeyExp.FindStringSubmatch(key) + if len(regexControlKeyMatch) == 0 { + continue + } + + // in the list? + if !stringInSlice(valueStr, allControls) { + allControls = append(allControls, valueStr) + } + } + + // for each control, find the single "winner" + for i := range allControls { + control := allControls[i] + + // iterate over the flattened map specifically for this control + for key, value := range *flattened { + // anything to do? + regexControlKeyMatch := regexControlKeyExp.FindStringSubmatch(key) + if len(regexControlKeyMatch) == 0 { + continue + } + if value.(string) != control { + continue + } + + // we simply take the *first* one as the winner. probably stupid. + normalizedKeyPrefix := fmt.Sprintf("controls%s%s", config.KeySeparator, control) + + // export the actual prefix to steal from the flattened map + regexControlKeyResult := make(map[string]string) + for i, controlKeyName := range regexControlKeyExp.SubexpNames() { + if i != 0 { + regexControlKeyResult[controlKeyName] = regexControlKeyMatch[i] + } + } + prefixMatch := regexControlKeyResult["prefix_match"] + + // get some other info we want to include with normalized data + initialPrefixMatch := regexControlKeyResult["initial_prefix_match"] + + // iterate over the flattened map...again + for key2, value2 := range *flattened { + // check for and export suffix + if strings.HasPrefix(key2, prefixMatch) { + // add normalized entry "as-is" + suffixMatch := key2[len(prefixMatch):] + newControlKey := fmt.Sprintf("%s%s%s", normalizedKeyPrefix, config.KeySeparator, suffixMatch) + (*flattened)[newControlKey] = value2 + continue + } + + // iterate over the component info + for componentCheckKey, componentCheckValue := range componentCheck { + componentCheckFullKey := initialPrefixMatch + config.KeySeparator + componentCheckKey + if key2 == componentCheckFullKey { + newControlKey := fmt.Sprintf("%s%scomponent%s%s", normalizedKeyPrefix, config.KeySeparator, config.KeySeparator, componentCheckValue) + (*flattened)[newControlKey] = value2 + } + } + } + } + } + + // we really don't error check here + return nil +} + +// flatten - generic function to flatten JSON or YAML +func flatten(config *Config, input map[string]interface{}, lkey string, flattened *map[string]interface{}) error { + // start the ball rolling + processed, err := flattenDriver(config, input, lkey, flattened) + if err != nil { + return err + } + if !processed { + return fmt.Errorf("flatten could not process '%v'", input) + } + + // the final part of flatten is to normalize control output + return flattenNormalize(config, flattened) +} diff --git a/pkg/cli/export/export_outputFormat.go b/pkg/cli/export/export_outputFormat.go new file mode 100644 index 00000000..bfc8bab8 --- /dev/null +++ b/pkg/cli/export/export_outputFormat.go @@ -0,0 +1,54 @@ +package export + +import ( + "errors" + "strings" +) + +//////////////////////////////////////////////////////////////////////// +// OutputFormat enumeration support + +// OutputFormat is the format to use for output file +type OutputFormat int + +// local variables to map to / from the enumeration +var outputFormatStrings []string +var outputFormats []OutputFormat + +// add string as new mapped enumeration +func ciota(s string) OutputFormat { + var result = OutputFormat(len(outputFormatStrings) - 1) + outputFormatStrings = append(outputFormatStrings, s) + outputFormats = append(outputFormats, result) + return result +} + +// create the enumerations +var ( + FormatUnset = ciota("") + FormatJSON = ciota("json") + FormatYAML = ciota("yaml") +) + +// Convert OutputFormat to string +func (f OutputFormat) String() string { + return outputFormatStrings[int(f)] +} + +// ToOutputFormat converts a string to an OutputFormat enum +func ToOutputFormat(s string) (OutputFormat, error) { + // sanity + if len(strings.TrimSpace(s)) == 0 { + return FormatUnset, errors.New("empty string") + } + + // scan + for i, v := range outputFormatStrings { + if v == s { + return outputFormats[i], nil + } + } + + // not found + return FormatUnset, errors.New("invalid value") +} diff --git a/pkg/cli/export/export_suite_test.go b/pkg/cli/export/export_suite_test.go new file mode 100644 index 00000000..35d332a6 --- /dev/null +++ b/pkg/cli/export/export_suite_test.go @@ -0,0 +1,13 @@ +package export_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestExport(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Export Suite") +} diff --git a/pkg/cli/export/export_test.go b/pkg/cli/export/export_test.go new file mode 100644 index 00000000..5a21440d --- /dev/null +++ b/pkg/cli/export/export_test.go @@ -0,0 +1,126 @@ +package export_test + +import ( + . "github.com/opencontrol/compliance-masonry/pkg/cli/export" + + "errors" + . "github.com/onsi/ginkgo" + "github.com/stretchr/testify/assert" + "os" + "path/filepath" +) + +var _ = Describe("Export", func() { + Describe("Verify Export functions", func() { + var ( + workingDir string + jsonFormat OutputFormat + yamlFormat OutputFormat + standardKeySeparator string + customKeySeparator string + ) + BeforeEach(func() { + workingDir, _ = os.Getwd() + jsonFormat, _ = ToOutputFormat("json") + yamlFormat, _ = ToOutputFormat("yaml") + standardKeySeparator = ":" + customKeySeparator = ".." + }) + Describe("bad inputs", func() { + Context("When no arguments are specified", func() { + It("should return an error", func() { + config := Config{} + err := Export(config) + assert.Equal(GinkgoT(), []error{errors.New("Error: Missing Certification Argument")}, err) + }) + }) + }) + Describe("standard processing", func() { + Context("JSON Export", func() { + It("should return no error", func() { + config := Config{ + Certification: "LATO", + OpencontrolDir: filepath.Join(workingDir, "..", "..", "..", "test", "fixtures", "opencontrol_fixtures_complete"), + DestinationFile: "-str-", + OutputFormat: jsonFormat, + } + err := Export(config) + assert.Nil(GinkgoT(), err) + }) + }) + Context("YAML Export", func() { + It("should return no error", func() { + config := Config{ + Certification: "LATO", + OpencontrolDir: filepath.Join(workingDir, "..", "..", "..", "test", "fixtures", "opencontrol_fixtures_complete"), + DestinationFile: "-str-", + OutputFormat: yamlFormat, + } + err := Export(config) + assert.Nil(GinkgoT(), err) + }) + }) + Context("JSON Export with flattening", func() { + It("should return no error", func() { + config := Config{ + Certification: "LATO", + OpencontrolDir: filepath.Join(workingDir, "..", "..", "..", "test", "fixtures", "opencontrol_fixtures_complete"), + DestinationFile: "-str-", + OutputFormat: jsonFormat, + Flatten: true, + KeySeparator: standardKeySeparator, + } + err := Export(config) + assert.Nil(GinkgoT(), err) + }) + }) + Context("JSON Export with flattening and key inference", func() { + It("should return no error", func() { + config := Config{ + Certification: "LATO", + OpencontrolDir: filepath.Join(workingDir, "..", "..", "..", "test", "fixtures", "opencontrol_fixtures_complete"), + DestinationFile: "-str-", + OutputFormat: jsonFormat, + Flatten: true, + InferKeys: true, + KeySeparator: standardKeySeparator, + } + err := Export(config) + assert.Nil(GinkgoT(), err) + }) + }) + Context("JSON Export with flattening and key inference; docxtemplater support", func() { + It("should return no error", func() { + config := Config{ + Certification: "LATO", + OpencontrolDir: filepath.Join(workingDir, "..", "..", "..", "test", "fixtures", "opencontrol_fixtures_complete"), + DestinationFile: "-str-", + OutputFormat: jsonFormat, + Flatten: true, + InferKeys: true, + Docxtemplater: true, + KeySeparator: standardKeySeparator, + } + err := Export(config) + assert.Nil(GinkgoT(), err) + }) + }) + Context("JSON Export with flattening and key inference; docxtemplater support and custom key separator", func() { + It("should return no error", func() { + config := Config{ + Certification: "LATO", + OpencontrolDir: filepath.Join(workingDir, "..", "..", "..", "test", "fixtures", "opencontrol_fixtures_complete"), + DestinationFile: "-str-", + OutputFormat: jsonFormat, + Flatten: true, + InferKeys: true, + Docxtemplater: true, + KeySeparator: customKeySeparator, + } + err := Export(config) + assert.Nil(GinkgoT(), err) + }) + }) + }) + }) +}) diff --git a/pkg/cli/export/export_utility.go b/pkg/cli/export/export_utility.go new file mode 100644 index 00000000..2ab500ba --- /dev/null +++ b/pkg/cli/export/export_utility.go @@ -0,0 +1,110 @@ +package export + +import ( + "strconv" + "strings" +) + +//////////////////////////////////////////////////////////////////////// +// Package functions + +// escapedPrefixMatch - escape a string in prep for regexp +func escapeStringForRegexp(a string) string { + specialChars := []string{"(", ")", ".", "?"} + for i := range specialChars { + specialChar := specialChars[i] + a = strings.Replace(a, specialChar, "\\"+specialChar, -1) + } + return a +} + +// stringInSlice - is a string in a list? +func stringInSlice(a string, list []string) bool { + for _, b := range list { + if b == a { + return true + } + } + return false +} + +// utility function to return an error list from single error +func returnErrors(err error) []error { + var result []error + result = append(result, err) + return result +} + +// debugHook - adds onto dlv with specific condition +func debugHook(config *Config, _ *map[string]interface{}) { + // could assume called only in debug mode; check anyway + if !config.Debug { + return + } + // insert whatever you need for a hook into stopping the program +} + +// isScalar - is a given value a supported scalar? +func isScalar(value interface{}) bool { + // first, check all supported simple types + result := false + if _, okStr := value.(string); okStr { + result = true + } else if _, okFloat64 := value.(float64); okFloat64 { + result = true + } else if _, okBool := value.(bool); okBool { + result = true + } + return result +} + +// flattenDiscoverKey - handle what a flattened array key should be +func discoverKey(config *Config, value interface{}, lkey string, index int) string { + // default value + defaultKey := lkey + strconv.Itoa(index) + + // only process if we must infer keys + if !config.InferKeys { + return defaultKey + } + + // we can only handle maps + input, okMapType := value.(map[string]interface{}) + if !okMapType { + return defaultKey + } + + // determine weights for the keyname to use + const invalidKeyName = "invalid" + const invalidKeyWeight = 99 + keyWeights := make(map[string]int) + keyWeights["key"] = 0 + keyWeights["control_key"] = 1 + keyWeights["name"] = 2 + + // iterate over the map, just looking at the keys + foundKeyWeight := invalidKeyWeight + foundKeyName := invalidKeyName + for rkey, rvalue := range input { + // must be a string scalar + _, isStr := rvalue.(string) + if !isStr { + continue + } + + if curKeyWeight, hasKey := keyWeights[rkey]; hasKey { + if curKeyWeight < foundKeyWeight { + foundKeyWeight = curKeyWeight + foundKeyName = rvalue.(string) + } + } + } + + // return the bestest key we can find + if foundKeyWeight != invalidKeyWeight { + return lkey + foundKeyName + } + + // return the default + return defaultKey +} diff --git a/pkg/cmd/masonry/cmd.go b/pkg/cmd/masonry/cmd.go index 2bcb7836..ec4a0e04 100644 --- a/pkg/cmd/masonry/cmd.go +++ b/pkg/cmd/masonry/cmd.go @@ -10,6 +10,7 @@ import ( "github.com/opencontrol/compliance-masonry/pkg/cli/clierrors" "github.com/opencontrol/compliance-masonry/pkg/cli/diff" "github.com/opencontrol/compliance-masonry/pkg/cli/docs" + "github.com/opencontrol/compliance-masonry/pkg/cli/export" "github.com/opencontrol/compliance-masonry/pkg/cli/get" "github.com/opencontrol/compliance-masonry/version" "github.com/spf13/cobra" @@ -44,6 +45,7 @@ the OpenControl Schema`, cmds.AddCommand(diff.NewCmdDiff(out)) cmds.AddCommand(docs.NewCmdDocs(out)) + cmds.AddCommand(export.NewCmdExport(out)) cmds.AddCommand(get.NewCmdGet(out)) // Global Options diff --git a/pkg/lib/certifications/certification.go b/pkg/lib/certifications/certification.go index f30cae9e..fc80f36f 100644 --- a/pkg/lib/certifications/certification.go +++ b/pkg/lib/certifications/certification.go @@ -1,6 +1,8 @@ package certifications import ( + "encoding/json" + "errors" v1_0_0 "github.com/opencontrol/compliance-masonry/pkg/lib/certifications/versions/1_0_0" "github.com/opencontrol/compliance-masonry/pkg/lib/common" "gopkg.in/yaml.v2" @@ -21,3 +23,22 @@ func Load(certificationFile string) (common.Certification, error) { } return certification, nil } + +// MarshalJSON accounts for different versions +func MarshalJSON(certification common.Certification) (b []byte, e error) { + // ABr: *punt* on getting marshal to work with interface + var ( + bytesCertification []byte + err error + ) + vCertification1_0_0, ok := certification.(v1_0_0.Certification) + if ok { + bytesCertification, err = json.Marshal(&vCertification1_0_0) + } else { + return nil, errors.New("unsupported certification version") + } + if err != nil { + return nil, err + } + return bytesCertification, nil +} diff --git a/pkg/lib/certifications/versions/1_0_0/certification.go b/pkg/lib/certifications/versions/1_0_0/certification.go index e133180c..40773a2b 100644 --- a/pkg/lib/certifications/versions/1_0_0/certification.go +++ b/pkg/lib/certifications/versions/1_0_0/certification.go @@ -1,6 +1,9 @@ package certification import ( + "bytes" + "encoding/json" + "fmt" "sort" "vbom.ml/util/sortorder" ) @@ -12,6 +15,24 @@ type Certification struct { Standards map[string]map[string]interface{} `yaml:"standards" json:"standards"` } +// MarshalJSON provides JSON support +func (p *Certification) MarshalJSON() (b []byte, e error) { + // start the marshaling + buffer := bytes.NewBufferString("{") + + // write data + buffer.WriteString(fmt.Sprintf("\"key\":\"%s\",\"standards\":", p.Key)) + bytes, err := json.Marshal(p.GetSortedStandards()) + if err != nil { + return nil, err + } + buffer.WriteString(string(bytes)) + + // done with marshaling + buffer.WriteString("}") + return buffer.Bytes(), nil +} + // GetKey returns the name of the certification. func (certification Certification) GetKey() string { return certification.Key diff --git a/pkg/lib/common/mocks/Workspace.go b/pkg/lib/common/mocks/Workspace.go index 441a8a84..89882a32 100644 --- a/pkg/lib/common/mocks/Workspace.go +++ b/pkg/lib/common/mocks/Workspace.go @@ -40,6 +40,22 @@ func (_m *Workspace) GetAllVerificationsWith(standardKey string, controlKey stri return r0 } +// GetAllStandards provides a mock function with given fields: +func (_m *Workspace) GetAllStandards() []common.Standard { + ret := _m.Called() + + var r0 []common.Standard + if rf, ok := ret.Get(0).(func() []common.Standard); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]common.Standard) + } + } + + return r0 +} + // GetCertification provides a mock function with given fields: func (_m *Workspace) GetCertification() common.Certification { ret := _m.Called() diff --git a/pkg/lib/common/workspace.go b/pkg/lib/common/workspace.go index 9934ac16..8b60900b 100644 --- a/pkg/lib/common/workspace.go +++ b/pkg/lib/common/workspace.go @@ -9,6 +9,7 @@ type Workspace interface { GetCertification() Certification GetAllComponents() []Component GetComponent(componentKey string) (Component, bool) + GetAllStandards() []Standard GetStandard(standardKey string) (Standard, bool) GetAllVerificationsWith(standardKey string, controlKey string) Verifications } diff --git a/pkg/lib/standards.go b/pkg/lib/standards.go index 117427a4..570b0bb2 100644 --- a/pkg/lib/standards.go +++ b/pkg/lib/standards.go @@ -57,6 +57,12 @@ func (ws *localWorkspace) LoadStandard(standardFile string) error { return nil } +// GetAllStandards retrieves all standards +func (ws *localWorkspace) GetAllStandards() []common.Standard { + return ws.standards.getAll() +} + +// GetStandard retrieves a specific standard func (ws *localWorkspace) GetStandard(standardKey string) (common.Standard, bool) { return ws.standards.get(standardKey) } diff --git a/tools/constants/constants.go b/tools/constants/constants.go index 1134300f..f17f3415 100644 --- a/tools/constants/constants.go +++ b/tools/constants/constants.go @@ -17,6 +17,12 @@ const ( DefaultExportsFolder = "exports" // DefaultMarkdownFolder is the folder containing markdown content DefaultMarkdownFolder = "markdowns" + // DefaultJSONFile is the file to store combined JSON + DefaultJSONFile = DefaultDestination + "/opencontrol.json" + // DefaultOutputFormat is the default format for general output + DefaultOutputFormat = "json" + // DefaultKeySeparator is the default separator for keys when flattening structure + DefaultKeySeparator = ":" ) // ResourceType is a type to help tell when it should be of only types of resources.