github.com/CycloneDX/sbom-utility@v0.16.0/schema/license_policy_config.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  /*
     3   * Licensed to the Apache Software Foundation (ASF) under one or more
     4   * contributor license agreements.  See the NOTICE file distributed with
     5   * this work for additional information regarding copyright ownership.
     6   * The ASF licenses this file to You under the Apache License, Version 2.0
     7   * (the "License"); you may not use this file except in compliance with
     8   * the License.  You may obtain a copy of the License at
     9   *
    10   *     http://www.apache.org/licenses/LICENSE-2.0
    11   *
    12   * Unless required by applicable law or agreed to in writing, software
    13   * distributed under the License is distributed on an "AS IS" BASIS,
    14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    15   * See the License for the specific language governing permissions and
    16   * limitations under the License.
    17   */
    18  
    19  package schema
    20  
    21  import (
    22  	"encoding/json"
    23  	"fmt"
    24  	"os"
    25  	"regexp"
    26  	"strings"
    27  	"sync"
    28  
    29  	"github.com/CycloneDX/sbom-utility/common"
    30  	"github.com/CycloneDX/sbom-utility/resources"
    31  	"github.com/CycloneDX/sbom-utility/utils"
    32  	"github.com/jwangsadinata/go-multimap/slicemultimap"
    33  )
    34  
    35  const (
    36  	POLICY_ALLOW        = "allow"
    37  	POLICY_DENY         = "deny"
    38  	POLICY_NEEDS_REVIEW = "needs-review"
    39  	POLICY_UNDEFINED    = "UNDEFINED"
    40  	POLICY_CONFLICT     = "CONFLICT"
    41  )
    42  
    43  var VALID_USAGE_POLICIES = []string{POLICY_ALLOW, POLICY_DENY, POLICY_NEEDS_REVIEW}
    44  var ALL_USAGE_POLICIES = []string{POLICY_ALLOW, POLICY_DENY, POLICY_NEEDS_REVIEW, POLICY_UNDEFINED, POLICY_CONFLICT}
    45  
    46  type LicensePolicy struct {
    47  	Id             string   `json:"id"`
    48  	Reference      string   `json:"reference"`
    49  	IsOsiApproved  bool     `json:"osi"`
    50  	IsFsfLibre     bool     `json:"fsf"`
    51  	IsDeprecated   bool     `json:"deprecated"`
    52  	Family         string   `json:"family"`
    53  	Name           string   `json:"name"`
    54  	UsagePolicy    string   `json:"usagePolicy"`
    55  	Aliases        []string `json:"aliases"`
    56  	Children       []string `json:"children"`
    57  	Notes          []string `json:"notes"`
    58  	Urls           []string `json:"urls"`
    59  	AnnotationRefs []string `json:"annotationRefs"`
    60  
    61  	// Alternative field names for --where searches
    62  	AltUsagePolicy    string `json:"usage-policy"`
    63  	AltAnnotationRefs string `json:"annotations"`
    64  	AltSPDXId         string `json:"spdx-id"`
    65  }
    66  
    67  type LicensePolicyConfig struct {
    68  	PolicyList              []LicensePolicy   `json:"policies"`
    69  	Annotations             map[string]string `json:"annotations"`
    70  	defaultPolicyConfigFile string
    71  	policyConfigFile        string
    72  	loadOnce                sync.Once
    73  	hashOnce                sync.Once
    74  	licenseFamilyNameMap    *slicemultimap.MultiMap
    75  	licenseIdMap            *slicemultimap.MultiMap
    76  	filteredFamilyNameMap   *slicemultimap.MultiMap
    77  }
    78  
    79  func NewLicensePolicyConfig(configFile string) *LicensePolicyConfig {
    80  	temp := LicensePolicyConfig{
    81  		defaultPolicyConfigFile: configFile,
    82  		policyConfigFile:        configFile,
    83  	}
    84  	return &temp
    85  }
    86  
    87  func (config *LicensePolicyConfig) Reset() {
    88  	config.policyConfigFile = config.defaultPolicyConfigFile
    89  	config.PolicyList = nil
    90  	config.Annotations = nil
    91  	if config.licenseFamilyNameMap != nil {
    92  		config.licenseFamilyNameMap.Clear()
    93  	}
    94  	if config.licenseIdMap != nil {
    95  		config.licenseIdMap.Clear()
    96  	}
    97  	if config.filteredFamilyNameMap != nil {
    98  		config.filteredFamilyNameMap.Clear()
    99  	}
   100  }
   101  
   102  func (config *LicensePolicyConfig) GetFamilyNameMap() (hashmap *slicemultimap.MultiMap, err error) {
   103  	if config.licenseFamilyNameMap == nil {
   104  		err = config.hashLicensePolicies()
   105  	}
   106  	return config.licenseFamilyNameMap, err
   107  }
   108  
   109  func (config *LicensePolicyConfig) GetLicenseIdMap() (hashmap *slicemultimap.MultiMap, err error) {
   110  	if config.licenseIdMap == nil {
   111  		err = config.hashLicensePolicies()
   112  	}
   113  	return config.licenseIdMap, err
   114  }
   115  
   116  func (config *LicensePolicyConfig) GetFilteredFamilyNameMap(whereFilters []common.WhereFilter) (hashmap *slicemultimap.MultiMap, err error) {
   117  	// NOTE: This call is necessary as this will cause all `licensePolicyConfig.PolicyList`
   118  	// entries to have alternative field names to be mapped (e.g., `usagePolicy` -> `usage-policy`)
   119  	config.filteredFamilyNameMap, err = config.GetFamilyNameMap()
   120  
   121  	if err != nil {
   122  		return
   123  	}
   124  
   125  	if len(whereFilters) > 0 {
   126  		// Always use a new filtered hashmap for each filtered list request
   127  		config.filteredFamilyNameMap = slicemultimap.New()
   128  		err = config.filteredHashLicensePolicies(whereFilters)
   129  	}
   130  	return config.filteredFamilyNameMap, err
   131  }
   132  
   133  func (config *LicensePolicyConfig) LoadHashPolicyConfigurationFile(policyFile string, defaultPolicyFile string) (err error) {
   134  	// Do not pass a default file, it should fail if custom policy cannot be loaded
   135  	// Only load the policy config. once
   136  	config.loadOnce.Do(func() {
   137  		err = config.innerLoadLicensePolicies(policyFile, defaultPolicyFile)
   138  		if err != nil {
   139  			return
   140  		}
   141  
   142  		// Note: the HashLicensePolicies function creates new id and name hashmaps
   143  		// therefore there is no need to clear them
   144  		err = config.hashLicensePolicies()
   145  	})
   146  
   147  	return
   148  }
   149  
   150  func (config *LicensePolicyConfig) innerLoadLicensePolicies(policyFile string, defaultPolicyFile string) (err error) {
   151  	getLogger().Enter(policyFile)
   152  	defer getLogger().Exit()
   153  
   154  	var buffer []byte
   155  
   156  	// Always reset the config if a new policy file is loaded
   157  	config.Reset()
   158  
   159  	if policyFile != "" {
   160  		// locate the license policy file
   161  		config.policyConfigFile, err = utils.FindVerifyConfigFileAbsPath(getLogger(), policyFile)
   162  
   163  		if err != nil {
   164  			return fmt.Errorf("unable to find license policy file: `%s`", policyFile)
   165  		}
   166  
   167  		// attempt to read in contents of the policy config.
   168  		getLogger().Infof("Loading license policy file: `%s`...", config.policyConfigFile)
   169  		buffer, err = os.ReadFile(config.policyConfigFile)
   170  		if err != nil {
   171  			return fmt.Errorf("unable to `ReadFile`: `%s`", config.policyConfigFile)
   172  		}
   173  	} else {
   174  		// Attempt to load the default config file from embedded file resources
   175  		getLogger().Infof("Loading (embedded) default license policy file: `%s`...", defaultPolicyFile)
   176  		buffer, err = resources.LoadConfigFile(defaultPolicyFile)
   177  		if err != nil {
   178  			return fmt.Errorf("unable to read schema config file: `%s` from embedded resources: `%s`",
   179  				defaultPolicyFile, resources.RESOURCES_CONFIG_DIR)
   180  		}
   181  	}
   182  
   183  	// NOTE: this cleverly unmarshals into the current config instance this function is associated with
   184  	errUnmarshal := json.Unmarshal(buffer, config)
   185  	if errUnmarshal != nil {
   186  		err = fmt.Errorf("cannot `Unmarshal`: `%s`", config.policyConfigFile)
   187  		return
   188  	}
   189  
   190  	return
   191  }
   192  
   193  func (config *LicensePolicyConfig) hashLicensePolicies() (hashError error) {
   194  	getLogger().Enter()
   195  	defer getLogger().Exit()
   196  
   197  	config.hashOnce.Do(func() {
   198  		hashError = config.innerHashLicensePolicies()
   199  	})
   200  	return
   201  }
   202  
   203  func (config *LicensePolicyConfig) innerHashLicensePolicies() (err error) {
   204  	getLogger().Enter()
   205  	defer getLogger().Exit()
   206  
   207  	// Note: we only need test to see if one of the maps has not been allocated
   208  	// and populated to infer neither has
   209  	config.licenseFamilyNameMap = slicemultimap.New()
   210  	config.licenseIdMap = slicemultimap.New()
   211  
   212  	for i, policy := range config.PolicyList {
   213  
   214  		// Map old JSON key names to new key names (as they appear as titles in report columns)
   215  
   216  		// Update the original entries in the []PolicyList stored in the global LicenseComplianceConfig
   217  		getLogger().Debugf("Mapping: `Id`: `%s` to `spdx-id`: `%s`\n", policy.Id, policy.AltSPDXId)
   218  		config.PolicyList[i].AltSPDXId = policy.Id
   219  		getLogger().Debugf("Mapping: `UsagePolicy`: `%s` to `usage-policy`: `%s`\n", policy.Id, policy.AltSPDXId)
   220  		config.PolicyList[i].AltUsagePolicy = policy.UsagePolicy
   221  		getLogger().Debugf("Mapping: `AnnotationRefs`: `%s` to `annotations`: `%s`\n", policy.Id, policy.AltSPDXId)
   222  		config.PolicyList[i].AltAnnotationRefs = strings.Join(policy.AnnotationRefs, ",")
   223  
   224  		// Actually hash the policy
   225  		err = config.hashPolicy(config.PolicyList[i])
   226  		if err != nil {
   227  			err = fmt.Errorf("unable to hash policy: %v", config.PolicyList[i])
   228  			return
   229  		}
   230  	}
   231  	return
   232  }
   233  
   234  // We will take the raw license policy and make it accessible for fast hash lookup
   235  // Multiple hash maps are created understanding that license data in SBOMs can be
   236  // based upon SPDX IDs <or> license names <or> license family names
   237  // NOTE: we allow for both discrete policies based upon SPDX ID as well as
   238  // "family" based policies.  This means given hash (lookup) might map to one or more
   239  // family policies as well as a discrete one for specific SPDX ID.  In such cases,
   240  // the policy MUST align (i.e., must not have both "allow" and "deny". Therefore,
   241  // when we hash we assure that such a conflict does NOT exist at time of creation.
   242  func (config *LicensePolicyConfig) hashPolicy(policy LicensePolicy) (err error) {
   243  	// ONLY hash valid policy records.
   244  	if !IsValidPolicyEntry(policy) {
   245  		// Do not add it to any hash table
   246  		getLogger().Tracef("WARNING: invalid policy entry (id: `%s`, name: `%s`). Skipping...", policy.Id, policy.Name)
   247  		return
   248  	}
   249  
   250  	// Only add to "id" hashmap if "Id" value is valid
   251  	// NOTE: do NOT hash entries with "" (empty) Id values; however, they may represent a "family" entry
   252  	if policy.Id != "" {
   253  		getLogger().Debugf("ID Hashmap: Adding policy Id=`%s`, Name=`%s`, Family=`%s`", policy.Id, policy.Name, policy.Family)
   254  		config.licenseIdMap.Put(policy.Id, policy)
   255  	} else {
   256  		getLogger().Debugf("WARNING: Skipping policy with no SPDX ID (empty)...")
   257  	}
   258  
   259  	// Assure we are not adding policy (value) to an existing hash
   260  	// that represents a policy conflict.
   261  	values, match := config.licenseFamilyNameMap.Get(policy.Family)
   262  
   263  	// If a hashmap entry exists, see if current policy matches those
   264  	// already added for that key
   265  	if match {
   266  		getLogger().Debugf("Family Hashmap: Entries exist for policy Id=`%s`, Name=`%s`, Family=`%s`", policy.Id, policy.Name, policy.Family)
   267  		consistent := VerifyPoliciesMatch(policy, values)
   268  
   269  		if !consistent {
   270  			err = getLogger().Errorf("Multiple (possibly conflicting) policies declared for ID `%s`,family: `%s`, policy: `%s`",
   271  				policy.Id,
   272  				policy.Family,
   273  				policy.UsagePolicy)
   274  			return
   275  		}
   276  	}
   277  
   278  	// NOTE: validation of policy struct (including "family" value) is done above
   279  	getLogger().Debugf("Family Hashmap: Adding policy Id=`%s`, Name=`%s`, Family=`%s`", policy.Id, policy.Name, policy.Family)
   280  
   281  	// Do NOT hash entries with and empty "Family" (name) value
   282  	if policy.Family != "" {
   283  		getLogger().Debugf("ID Hashmap: Adding policy Id=`%s`, Name=`%s`, Family=`%s`", policy.Id, policy.Name, policy.Family)
   284  		config.licenseFamilyNameMap.Put(policy.Family, policy)
   285  	} else {
   286  		err = getLogger().Errorf("invalid policy: Family: \"\" (empty)")
   287  		return
   288  	}
   289  
   290  	if len(policy.Children) > 0 {
   291  		err = config.hashChildPolicies(policy)
   292  		if err != nil {
   293  			return
   294  		}
   295  	}
   296  
   297  	return
   298  }
   299  
   300  func (config *LicensePolicyConfig) hashChildPolicies(policy LicensePolicy) (err error) {
   301  
   302  	for _, childId := range policy.Children {
   303  		// Copy of the parent policy and overwrite its "id" with the child's
   304  		childPolicy := policy
   305  		childPolicy.Id = childId
   306  		// Do NOT copy children as this will break recursion
   307  		childPolicy.Children = nil
   308  		// No need to copy Notes and Urls which carry "family" information and links
   309  		childPolicy.Notes = nil
   310  		childPolicy.Urls = nil
   311  
   312  		err = config.hashPolicy(childPolicy)
   313  		if err != nil {
   314  			return
   315  		}
   316  	}
   317  
   318  	return
   319  }
   320  
   321  func (config *LicensePolicyConfig) filteredHashLicensePolicies(whereFilters []common.WhereFilter) (err error) {
   322  	getLogger().Enter()
   323  	defer getLogger().Exit(err)
   324  
   325  	// NOTE: original []PolicyList includes values for both deprecated and current fields
   326  	// So that filtered "queries" will work regardless (for backwards compatibility)
   327  	for _, policy := range config.PolicyList {
   328  		err = config.filteredHashLicensePolicy(policy, whereFilters)
   329  		if err != nil {
   330  			return
   331  		}
   332  	}
   333  	return
   334  }
   335  
   336  // Hash a CDX Component and recursively those of any "nested" components
   337  // TODO we should WARN if version is not a valid semver (e.g., examples/cyclonedx/BOM/laravel-7.12.0/bom.1.3.json)
   338  func (config *LicensePolicyConfig) filteredHashLicensePolicy(policy LicensePolicy, whereFilters []common.WhereFilter) (err error) {
   339  	var match bool = true
   340  	var mapPolicy map[string]interface{}
   341  
   342  	// See if the policy matches where filters criteria
   343  	if len(whereFilters) > 0 {
   344  		mapPolicy, err = utils.MarshalStructToJsonMap(policy)
   345  		if err != nil {
   346  			return
   347  		}
   348  
   349  		match, err = whereFilterMatch(mapPolicy, whereFilters)
   350  		if err != nil {
   351  			return
   352  		}
   353  	}
   354  
   355  	// Hash policy if it matched where filters
   356  	if match {
   357  		getLogger().Debugf("Matched: Hashing Policy: id: %s, family: %s", policy.Id, policy.Family)
   358  		config.filteredFamilyNameMap.Put(policy.Family, policy)
   359  	}
   360  
   361  	return
   362  }
   363  
   364  func (config *LicensePolicyConfig) FindPolicy(licenseInfo LicenseInfo) (matchedPolicy LicensePolicy, err error) {
   365  	getLogger().Enter()
   366  	defer getLogger().Exit()
   367  
   368  	// Initialize to empty
   369  	matchedPolicy = LicensePolicy{}
   370  
   371  	switch licenseInfo.LicenseChoiceTypeValue {
   372  	case LC_TYPE_ID:
   373  		matchedPolicy.UsagePolicy, matchedPolicy, err = config.FindPolicyBySpdxId(licenseInfo.LicenseChoice.License.Id)
   374  		if err != nil {
   375  			return
   376  		}
   377  	case LC_TYPE_NAME:
   378  		matchedPolicy.UsagePolicy, matchedPolicy, err = config.FindPolicyByFamilyName(licenseInfo.LicenseChoice.License.Name)
   379  		if err != nil {
   380  			return
   381  		}
   382  	case LC_TYPE_EXPRESSION:
   383  		// Parse expression according to SPDX spec.
   384  		var expressionTree *CompoundExpression
   385  		expressionTree, err = ParseExpression(config, licenseInfo.LicenseChoice.Expression)
   386  		if err != nil {
   387  			return
   388  		}
   389  		getLogger().Debugf("Parsed expression:\n%v", expressionTree)
   390  		matchedPolicy.UsagePolicy = expressionTree.CompoundUsagePolicy
   391  	}
   392  
   393  	if matchedPolicy.UsagePolicy == "" {
   394  		matchedPolicy.UsagePolicy = POLICY_UNDEFINED
   395  	}
   396  	//return matchedPolicy, err
   397  	return
   398  }
   399  
   400  func (config *LicensePolicyConfig) FindPolicyBySpdxId(id string) (policyValue string, matchedPolicy LicensePolicy, err error) {
   401  	getLogger().Enter("id:", id)
   402  	defer getLogger().Exit()
   403  
   404  	var matched bool
   405  	var arrPolicies []interface{}
   406  
   407  	// Note: this will cause all policy hashmaps to be initialized (created), if it has not bee
   408  	licensePolicyIdMap, err := config.GetLicenseIdMap()
   409  	if err != nil {
   410  		err = getLogger().Errorf("license policy map error: `%w`", err)
   411  		return
   412  	}
   413  
   414  	arrPolicies, matched = licensePolicyIdMap.Get(id)
   415  	getLogger().Tracef("licensePolicyMapById.Get(%s): (%v) matches", id, len(arrPolicies))
   416  
   417  	// There MUST be ONLY one policy per (discrete) license ID
   418  	if len(arrPolicies) > 1 {
   419  		err = getLogger().Errorf("Multiple (possibly conflicting) policies declared for SPDX ID=`%s`", id)
   420  		return
   421  	}
   422  
   423  	if matched {
   424  		// retrieve the usage policy from the single (first) entry
   425  		matchedPolicy = arrPolicies[0].(LicensePolicy)
   426  		policyValue = matchedPolicy.UsagePolicy
   427  	} else {
   428  		getLogger().Tracef("No policy match found for SPDX ID=`%s` ", id)
   429  		policyValue = POLICY_UNDEFINED
   430  	}
   431  
   432  	return
   433  }
   434  
   435  // NOTE: for now, we will look for the "family" name encoded in the License.Name field
   436  // (until) we can get additional fields/properties added to the CDX LicenseChoice schema
   437  func (config *LicensePolicyConfig) FindPolicyByFamilyName(name string) (policyValue string, matchedPolicy LicensePolicy, err error) {
   438  	getLogger().Enter("name:", name)
   439  	defer getLogger().Exit()
   440  
   441  	var matched bool
   442  	var key string
   443  	var arrPolicies []interface{}
   444  
   445  	// NOTE: we have found some SBOM authors have placed license expressions
   446  	// within the "name" field.  This prevents us from assigning policy
   447  	// return
   448  	if hasLogicalConjunctionOrPreposition(name) {
   449  		getLogger().Warningf("policy name contains logical conjunctions or preposition: `%s`", name)
   450  		policyValue = POLICY_UNDEFINED
   451  		return
   452  	}
   453  
   454  	// Note: this will cause all policy hashmaps to be initialized (created), if it has not been
   455  	familyNameMap, _ := config.GetFamilyNameMap()
   456  
   457  	// See if any of the policy family keys contain the family name
   458  	matched, key, err = config.searchForLicenseFamilyName(name)
   459  	if err != nil {
   460  		return
   461  	}
   462  
   463  	if matched {
   464  		arrPolicies, _ = familyNameMap.Get(key)
   465  
   466  		if len(arrPolicies) == 0 {
   467  			err = getLogger().Errorf("No policy match found in hashmap for family name key: `%s`", key)
   468  			return
   469  		}
   470  
   471  		// NOTE: We can use the first policy (of a family) as they are
   472  		// verified to be consistent when loaded from the policy config. file
   473  		matchedPolicy = arrPolicies[0].(LicensePolicy)
   474  		policyValue = matchedPolicy.UsagePolicy
   475  
   476  		// If we have more than one license in the same family (name), then
   477  		// check if there are any "usage policy" conflicts to display in report
   478  		if len(arrPolicies) > 1 {
   479  			conflict := policyConflictExists(arrPolicies)
   480  			if conflict {
   481  				getLogger().Tracef("Usage policy conflict for license family name=`%s` ", name)
   482  				policyValue = POLICY_CONFLICT
   483  			}
   484  		}
   485  	} else {
   486  		getLogger().Tracef("No policy match found for license family name=`%s` ", name)
   487  		policyValue = POLICY_UNDEFINED
   488  	}
   489  
   490  	return
   491  }
   492  
   493  // Loop through all known license family names (in hashMap) to see if any
   494  // appear in the CDX License "Name" field
   495  func (config *LicensePolicyConfig) searchForLicenseFamilyName(licenseName string) (found bool, familyName string, err error) {
   496  	getLogger().Enter()
   497  	defer getLogger().Exit()
   498  
   499  	familyNameMap, err := config.GetFamilyNameMap()
   500  	if err != nil {
   501  		getLogger().Error(err)
   502  		return
   503  	}
   504  
   505  	keys := familyNameMap.Keys()
   506  
   507  	for _, key := range keys {
   508  		familyName = key.(string)
   509  		getLogger().Debugf("Searching for familyName: '%s' in License Name: %s", familyName, licenseName)
   510  		found = containsFamilyName(licenseName, familyName)
   511  
   512  		if found {
   513  			getLogger().Debugf("Match found: familyName: '%s' in License Name: %s", familyName, licenseName)
   514  			return
   515  		}
   516  	}
   517  
   518  	return
   519  }
   520  
   521  //------------------------------------------------
   522  // License Policy "helper" functions
   523  //------------------------------------------------
   524  
   525  func IsValidUsagePolicy(usagePolicy string) bool {
   526  	for _, entry := range VALID_USAGE_POLICIES {
   527  		if usagePolicy == entry {
   528  			return true
   529  		}
   530  	}
   531  	return false
   532  }
   533  
   534  // NOTE: policy.Id == "" we allow as "valid" as this indicates a potential "family" entry (i.e., group of SPDX IDs)
   535  func IsValidPolicyEntry(policy LicensePolicy) bool {
   536  
   537  	if policy.Id != "" && !IsValidSpdxId(policy.Id) {
   538  		getLogger().Warningf("invalid SPDX ID: `%s` (Name=`%s`). Skipping...", policy.Id, policy.Name)
   539  		return false
   540  	}
   541  
   542  	if strings.TrimSpace(policy.Name) == "" {
   543  		getLogger().Warningf("invalid Name: `%s` (Id=`%s`).", policy.Name, policy.Id)
   544  	}
   545  
   546  	if !IsValidUsagePolicy(policy.UsagePolicy) {
   547  		getLogger().Warningf("invalid Usage Policy: `%s` (Id=`%s`, Name=`%s`). Skipping...", policy.UsagePolicy, policy.Id, policy.Name)
   548  		return false
   549  	}
   550  
   551  	if !IsValidFamilyKey(policy.Family) {
   552  		getLogger().Warningf("invalid Family: `%s` (Id=`%s`, Name=`%s`). Skipping...", policy.Family, policy.Id, policy.Name)
   553  		return false
   554  	}
   555  
   556  	if policy.Id == "" {
   557  		if len(policy.Children) < 1 {
   558  			getLogger().Debugf("Family (policy): `%s`. Has no children (SPDX IDs) listed.", policy.Family)
   559  		}
   560  		// Test to make sure "family" entries (i.e. policy.Id == "") have valid "children" (SPDX IDs)
   561  		for _, childId := range policy.Children {
   562  			if !IsValidSpdxId(childId) {
   563  				getLogger().Warningf("invalid Id: `%s` for Family: `%s`. Skipping...", childId, policy.Family)
   564  			}
   565  		}
   566  	}
   567  
   568  	// TODO - make sure policies with valid "Id" do NOT have children as these are
   569  	// intended to be discrete (non-family-grouped) entries
   570  	return true
   571  }
   572  
   573  // given an array of policies verify their "usage" policy does not represent a conflict
   574  func VerifyPoliciesMatch(testPolicy LicensePolicy, policies []interface{}) bool {
   575  
   576  	var currentPolicy LicensePolicy
   577  	testUsagePolicy := testPolicy.UsagePolicy
   578  
   579  	for _, current := range policies {
   580  		currentPolicy = current.(LicensePolicy)
   581  		getLogger().Debugf("Usage Policy=%s", currentPolicy.UsagePolicy)
   582  
   583  		if currentPolicy.UsagePolicy != testUsagePolicy {
   584  			getLogger().Warningf("Policy (Id: %s, Family: %s, Policy: %s) is in conflict with policies (%s) declared in the same family.",
   585  				currentPolicy.Id,
   586  				currentPolicy.Family,
   587  				currentPolicy.UsagePolicy,
   588  				testUsagePolicy)
   589  		}
   590  	}
   591  
   592  	return true
   593  }
   594  
   595  // NOTE: caller assumes resp. for checking for empty input array
   596  func policyConflictExists(arrPolicies []interface{}) bool {
   597  	var currentUsagePolicy string
   598  	var policy LicensePolicy
   599  
   600  	// Init. usage policy to first entry in array
   601  	policy = arrPolicies[0].(LicensePolicy)
   602  	currentUsagePolicy = policy.UsagePolicy
   603  
   604  	// Check every subsequent usage policy in array to identify mismatch (i.e., a conflict)
   605  	for i := 1; i < len(arrPolicies); i++ {
   606  		policy = arrPolicies[i].(LicensePolicy)
   607  		if policy.UsagePolicy != currentUsagePolicy {
   608  			return true
   609  		}
   610  	}
   611  	return false
   612  }
   613  
   614  // Looks for an SPDX family (name) somewhere in the CDX License object "Name" field
   615  func containsFamilyName(name string, familyName string) bool {
   616  	// NOTE: we do not currently normalize as we assume family names
   617  	// are proper substring of SPDX IDs which are mixed case and
   618  	// should match exactly as encoded.
   619  	return strings.Contains(name, familyName)
   620  }
   621  
   622  // Supported conjunctions and prepositions
   623  const (
   624  	AND                   string = "AND"
   625  	OR                    string = "OR"
   626  	WITH                  string = "WITH"
   627  	CONJUNCTION_UNDEFINED string = ""
   628  )
   629  
   630  func hasLogicalConjunctionOrPreposition(value string) bool {
   631  
   632  	if strings.Contains(value, AND) ||
   633  		strings.Contains(value, OR) ||
   634  		strings.Contains(value, WITH) {
   635  		return true
   636  	}
   637  	return false
   638  }
   639  
   640  //------------------------------------------------
   641  // CDX LicenseChoice "helper" functions
   642  //------------------------------------------------
   643  
   644  // "getter" for compiled regex expression
   645  func getRegexForValidSpdxId() (regex *regexp.Regexp, err error) {
   646  	if spdxIdRegexp == nil {
   647  		regex, err = regexp.Compile(REGEX_VALID_SPDX_ID)
   648  	}
   649  	return
   650  }
   651  
   652  func IsValidSpdxId(id string) bool {
   653  	regex, err := getRegexForValidSpdxId()
   654  	if err != nil {
   655  		getLogger().Error(fmt.Errorf("unable to invoke regex. %v", err))
   656  		return false
   657  	}
   658  	return regex.MatchString(id)
   659  }
   660  
   661  func IsValidFamilyKey(key string) bool {
   662  	var BAD_KEYWORDS = []string{"CONFLICT", "UNKNOWN"}
   663  
   664  	// For now, valid family keys are subsets of SPDX IDs
   665  	// Therefore, pass result from that SPDX ID validation function
   666  	valid := IsValidSpdxId(key)
   667  
   668  	// Test for keywords that we have seen set that clearly are not valid family names
   669  	// TODO: make keywords configurable
   670  	for _, keyword := range BAD_KEYWORDS {
   671  		if strings.Contains(strings.ToLower(key), strings.ToLower(keyword)) {
   672  			return false
   673  		}
   674  	}
   675  
   676  	return valid
   677  }