opencontrol/compliance-masonry

View on GitHub
pkg/cli/export/export_flatten.go

Summary

Maintainability
C
1 day
Test Coverage
/*
 Copyright (C) 2018 OpenControl Contributors. See LICENSE.md for license.
*/

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 {
        log.Printf("flatten:Scalar(string): %s=%s\n", key, value.(string))
        (*flattened)[key] = value.(string)
    } else if _, okFloat64 := value.(float64); okFloat64 {
        log.Printf("flatten:Scalar(float64): %s=%f\n", key, value.(float64))
        (*flattened)[key] = value.(float64)
    } else if _, okBool := value.(bool); okBool {
        log.Printf("flatten:Scalar(bool): %s=%t\n", key, value.(bool))
        (*flattened)[key] = value.(bool)
    } else {
        result = false
    }
    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
    }
    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 {
            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)
            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)
        }

        // 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 {
                    log.Printf("flatten:Array:embedArray:replaceEmptyKey %s\n", key)
                    (*flattenedToUse)["name"] = val
                    delete((*flattenedToUse), "")
                }
            }
            targetArray = append(targetArray, *flattenedToUse)
        }
    }

    // if we are using docxtemplater format, append targetArray as single value for this key
    if config.Docxtemplater && (targetArray != nil) {
        log.Printf("flatten:Array:useTargetArray %s\n", key)
        (*flattened)[key] = targetArray
    }

    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
    }
    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 {
            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)
        }
    }

    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 {
        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
    }

    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<prefix_match>(?P<initial_prefix_match>data" + config.KeySeparator +
        "components" + config.KeySeparator + "(?P<comp_name>.*?))" + config.KeySeparator +
        "satisfies" + config.KeySeparator + "(?P<control_key>.*?)" +
        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)
}