github.com/CycloneDX/sbom-utility@v0.16.0/schema/license_expression.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  	"strings"
    23  )
    24  
    25  type CompoundExpression struct {
    26  	SimpleLeft          string
    27  	SimpleLeftHasPlus   bool
    28  	LeftPolicy          LicensePolicy
    29  	LeftUsagePolicy     string
    30  	SimpleRight         string
    31  	SimpleRightHasPlus  bool
    32  	RightPolicy         LicensePolicy
    33  	RightUsagePolicy    string
    34  	Conjunction         string
    35  	PrepRight           string
    36  	PrepLeft            string
    37  	CompoundLeft        *CompoundExpression
    38  	CompoundRight       *CompoundExpression
    39  	CompoundUsagePolicy string
    40  }
    41  
    42  // Tokens
    43  const (
    44  	LEFT_PARENS                 string = "("
    45  	RIGHT_PARENS                string = ")"
    46  	LEFT_PARENS_WITH_SEPARATOR  string = "( "
    47  	RIGHT_PARENS_WITH_SEPARATOR string = " )"
    48  	PLUS_OPERATOR               string = "+"
    49  )
    50  
    51  const (
    52  	MSG_LICENSE_INVALID_EXPRESSION             = "invalid license expression"
    53  	MSG_LICENSE_EXPRESSION_INVALID_CONJUNCTION = "invalid conjunction"
    54  	MSG_LICENSE_EXPRESSION_UNDEFINED_POLICY    = "contains an undefined policy"
    55  	MSG_LICENSE_EXPRESSION                     = "license expression"
    56  )
    57  
    58  func NewCompoundExpression() *CompoundExpression {
    59  	ce := new(CompoundExpression)
    60  	ce.LeftUsagePolicy = POLICY_UNDEFINED
    61  	ce.RightUsagePolicy = POLICY_UNDEFINED
    62  	ce.CompoundUsagePolicy = POLICY_UNDEFINED
    63  	return ce
    64  }
    65  
    66  func tokenizeExpression(expression string) (tokens []string) {
    67  	// Add spaces to assure proper tokenization with whitespace bw/ tokens
    68  	expression = strings.ReplaceAll(expression, LEFT_PARENS, LEFT_PARENS_WITH_SEPARATOR)
    69  	expression = strings.ReplaceAll(expression, RIGHT_PARENS, RIGHT_PARENS_WITH_SEPARATOR)
    70  	// fields are, by default, separated by whitespace
    71  	tokens = strings.Fields(expression)
    72  	return
    73  }
    74  
    75  func ParseExpression(policyConfig *LicensePolicyConfig, rawExpression string) (ce *CompoundExpression, err error) {
    76  	getLogger().Enter()
    77  	defer getLogger().Exit()
    78  
    79  	ce = NewCompoundExpression()
    80  
    81  	tokens := tokenizeExpression(rawExpression)
    82  	getLogger().Debugf("Tokens: %v", tokens)
    83  
    84  	finalIndex, err := parseCompoundExpression(policyConfig, ce, tokens, 0)
    85  	getLogger().Debugf("Parsed expression (%v): %v", finalIndex, ce)
    86  
    87  	return ce, err
    88  }
    89  
    90  // NOTE: This expression parser MAY NOT account for multiple (>1) conjunctions
    91  // within a compound expression (e.g., Foo OR Bar AND Bqu) as this has not been endorsed
    92  // by the specification or any known examples.  However, we have put in place some
    93  // tests that shows the parser still works in these cases.
    94  func parseCompoundExpression(policyConfig *LicensePolicyConfig, expression *CompoundExpression, tokens []string, index int) (i int, err error) {
    95  	getLogger().Enter("expression:", expression)
    96  	defer getLogger().Exit()
    97  	defer func() {
    98  		if expression.CompoundUsagePolicy == POLICY_UNDEFINED {
    99  			getLogger().Warningf("%s: %s: expression: left term: %s, right term: %s",
   100  				MSG_LICENSE_EXPRESSION,
   101  				MSG_LICENSE_EXPRESSION_UNDEFINED_POLICY,
   102  				expression.LeftUsagePolicy,
   103  				expression.RightUsagePolicy,
   104  			)
   105  		}
   106  	}()
   107  	var token string
   108  	for index < len(tokens) {
   109  		token = tokens[index]
   110  		switch token {
   111  		case LEFT_PARENS:
   112  			getLogger().Debugf("[%v] LEFT_PARENS: `%v`", index, token)
   113  			childExpression := NewCompoundExpression()
   114  
   115  			// if we have no conjunction, this compound expression represents the "left" operand
   116  			if expression.Conjunction == "" {
   117  				expression.CompoundLeft = childExpression
   118  			} else {
   119  				// otherwise it is the "right" operand
   120  				expression.CompoundRight = childExpression
   121  			}
   122  
   123  			index, err = parseCompoundExpression(policyConfig, childExpression, tokens, index+1)
   124  			if err != nil {
   125  				return
   126  			}
   127  
   128  			// retrieve the resolved policy from the child
   129  			childPolicy := childExpression.CompoundUsagePolicy
   130  			if expression.Conjunction == "" {
   131  				expression.LeftUsagePolicy = childPolicy
   132  			} else {
   133  				// otherwise it is the "right" operand
   134  				expression.RightUsagePolicy = childPolicy
   135  			}
   136  
   137  		case RIGHT_PARENS:
   138  			getLogger().Debugf("[%v] RIGHT_PARENS: `%v`", index, token)
   139  			err = FinalizeCompoundPolicy(expression)
   140  			return index, err // Do NOT Increment, parent caller will do that
   141  		case AND:
   142  			getLogger().Debugf("[%v] AND (Conjunction): `%v`", index, token)
   143  			expression.Conjunction = token
   144  		case OR:
   145  			getLogger().Debugf("[%v] OR (Conjunction): `%v`", index, token)
   146  			expression.Conjunction = token
   147  		case WITH:
   148  			getLogger().Debugf("[%v] WITH (Preposition): `%v`", index, token)
   149  			if expression.Conjunction == "" {
   150  				expression.PrepLeft = token
   151  			} else {
   152  				// otherwise it is the "right" operand
   153  				expression.PrepRight = token
   154  			}
   155  		default:
   156  			getLogger().Debugf("[%v] Simple Expression: `%v`", index, token)
   157  			// if we have no conjunction, this compound expression represents the "left" operand
   158  			if expression.Conjunction == CONJUNCTION_UNDEFINED {
   159  				if expression.PrepLeft == "" {
   160  					expression.SimpleLeft = token
   161  					// Also, check for the unary "plus" operator
   162  					expression.SimpleLeftHasPlus = hasUnaryPlusOperator(token)
   163  					// Lookup policy in hashmap
   164  					expression.LeftUsagePolicy, expression.LeftPolicy, err = policyConfig.FindPolicyBySpdxId(token)
   165  					if err != nil {
   166  						return
   167  					}
   168  				} else {
   169  					// this token is a preposition, for now overload its value
   170  					expression.PrepLeft = token
   171  				}
   172  			} else {
   173  				// otherwise it is the "right" operand
   174  				if expression.PrepRight == "" {
   175  					expression.SimpleRight = token
   176  					// Also, check for the unary "plus" operator
   177  					expression.SimpleRightHasPlus = hasUnaryPlusOperator(token)
   178  					// Lookup policy in hashmap
   179  					expression.RightUsagePolicy, expression.RightPolicy, err = policyConfig.FindPolicyBySpdxId(token)
   180  					if err != nil {
   181  						return
   182  					}
   183  				} else {
   184  					// this token is a preposition, for now overload its value
   185  					expression.PrepRight = token
   186  				}
   187  			}
   188  		}
   189  
   190  		index = index + 1
   191  	}
   192  
   193  	err = FinalizeCompoundPolicy(expression)
   194  	return index, err
   195  }
   196  
   197  func FinalizeCompoundPolicy(expression *CompoundExpression) (err error) {
   198  	getLogger().Enter()
   199  	defer getLogger().Exit()
   200  
   201  	if expression == nil {
   202  		return getLogger().Errorf("Expression is nil")
   203  	}
   204  
   205  	getLogger().Debugf("Evaluating policy: (`%s` `%s` `%s`)",
   206  		expression.LeftUsagePolicy,
   207  		expression.Conjunction,
   208  		expression.RightUsagePolicy)
   209  
   210  	// The policy config. has 3 states: { "allow", "deny", "needs-review" }; n=3
   211  	// which are always paired with a conjunctions; r=2
   212  	// and for evaluation, we do not care about order.  This means we have to
   213  	// account for 6 combinations with unique results (policy determinations)
   214  	switch expression.Conjunction {
   215  	// The AND case, is considered "pessimistic"; that is, we want to quickly identify "negative" usage policies.
   216  	// This means we first look for any "deny" policy as this overrides any other state's value
   217  	// then look for any "needs-review" policy as we assume it COULD be a "deny" determination upon review
   218  	// this leaves the remaining state which is "allow" (both sides) as the only "positive" outcome
   219  	case AND:
   220  		// Undefined Short-circuit:
   221  		// If either left or right policy is UNDEFINED with the AND conjunction,
   222  		// take the pessimistic value (DENY) result if offered by either term
   223  		if expression.LeftUsagePolicy == POLICY_UNDEFINED ||
   224  			expression.RightUsagePolicy == POLICY_UNDEFINED {
   225  
   226  			if expression.LeftUsagePolicy == POLICY_DENY ||
   227  				expression.RightUsagePolicy == POLICY_DENY {
   228  				expression.CompoundUsagePolicy = POLICY_DENY
   229  
   230  			} else {
   231  				expression.CompoundUsagePolicy = POLICY_UNDEFINED
   232  			}
   233  			return nil
   234  		}
   235  
   236  		// This "deny" comparator block covers 3 of the 6 combinations:
   237  		// 1. POLICY_DENY AND POLICY_ALLOW
   238  		// 2. POLICY_DENY AND POLICY_NEEDS_REVIEW
   239  		// 3. POLICY_DENY AND POLICY_DENY
   240  		if expression.LeftUsagePolicy == POLICY_DENY ||
   241  			expression.RightUsagePolicy == POLICY_DENY {
   242  			expression.CompoundUsagePolicy = POLICY_DENY
   243  		} else if expression.LeftUsagePolicy == POLICY_NEEDS_REVIEW ||
   244  			expression.RightUsagePolicy == POLICY_NEEDS_REVIEW {
   245  			// This "needs-review" comparator covers 2 of the 6 combinations:
   246  			// 4. POLICY_NEEDS_REVIEW AND POLICY_ALLOW
   247  			// 5. POLICY_NEEDS_REVIEW AND POLICY_NEEDS_REVIEW
   248  			expression.CompoundUsagePolicy = POLICY_NEEDS_REVIEW
   249  		} else {
   250  			// This leaves the only remaining combination:
   251  			// 6. POLICY_ALLOW AND POLICY_ALLOW
   252  			expression.CompoundUsagePolicy = POLICY_ALLOW
   253  		}
   254  	// The OR case, is considered "optimistic"; that is, we want to quickly identify "positive" usage policies.
   255  	// This means we first look for any "allow" policy as this overrides any other state's value
   256  	// then look for any "needs-review" policy as we assume it COULD be an "allow" determination upon review
   257  	// this leaves the remaining state which is "allow" (both sides) as the only "positive" outcome
   258  	case OR:
   259  		// Undefined Short-circuit:
   260  		// If either left or right policy is UNDEFINED with the OR conjunction,
   261  		// take the result offered by the other term (which could also be UNDEFINED)
   262  		if expression.LeftUsagePolicy == POLICY_UNDEFINED {
   263  			// default to right policy (regardless of value)
   264  			expression.CompoundUsagePolicy = expression.RightUsagePolicy
   265  			getLogger().Debugf("Left usage policy is UNDEFINED")
   266  			return nil
   267  		} else if expression.RightUsagePolicy == POLICY_UNDEFINED {
   268  			// default to left policy (regardless of value)
   269  			expression.CompoundUsagePolicy = expression.LeftUsagePolicy
   270  			getLogger().Debugf("Right usage policy is UNDEFINED")
   271  			return nil
   272  		}
   273  
   274  		// This "allow" comparator block covers 3 of the 6 combinations:
   275  		// 1. POLICY_ALLOW OR POLICY_DENY
   276  		// 2. POLICY_ALLOW OR POLICY_NEEDS_REVIEW
   277  		// 3. POLICY_ALLOW OR POLICY_ALLOW
   278  		if expression.LeftUsagePolicy == POLICY_ALLOW ||
   279  			expression.RightUsagePolicy == POLICY_ALLOW {
   280  			expression.CompoundUsagePolicy = POLICY_ALLOW
   281  		} else if expression.LeftUsagePolicy == POLICY_NEEDS_REVIEW ||
   282  			expression.RightUsagePolicy == POLICY_NEEDS_REVIEW {
   283  			// This "needs-review" comparator covers 2 of the 6 combinations:
   284  			// 4. POLICY_NEEDS_REVIEW OR POLICY_DENY
   285  			// 5. POLICY_NEEDS_REVIEW OR POLICY_NEEDS_REVIEW
   286  			expression.CompoundUsagePolicy = POLICY_NEEDS_REVIEW
   287  		} else {
   288  			// This leaves the only remaining combination:
   289  			// 6. POLICY_DENY OR POLICY_DENY
   290  			expression.CompoundUsagePolicy = POLICY_DENY
   291  		}
   292  	case CONJUNCTION_UNDEFINED:
   293  		// Test for single compound expression (i.e., "(" compound-expression ")" )
   294  		// which is the only valid one that does not have an AND, OR or WITH conjunction
   295  		if expression.LeftUsagePolicy != POLICY_UNDEFINED &&
   296  			expression.RightUsagePolicy == POLICY_UNDEFINED {
   297  			expression.CompoundUsagePolicy = expression.LeftUsagePolicy
   298  		} // else default expression.CompoundUsagePolicy is UNDEFINED
   299  	default:
   300  		expression.CompoundUsagePolicy = POLICY_UNDEFINED
   301  		return getLogger().Errorf("%s: %s: `%s`",
   302  			MSG_LICENSE_INVALID_EXPRESSION,
   303  			MSG_LICENSE_EXPRESSION_INVALID_CONJUNCTION,
   304  			expression.Conjunction)
   305  
   306  	}
   307  	getLogger().Debugf("(%s (%s) %s %s (%s)) == %s",
   308  		expression.SimpleLeft,
   309  		expression.LeftUsagePolicy,
   310  		expression.Conjunction,
   311  		expression.SimpleRight,
   312  		expression.RightUsagePolicy,
   313  		expression.CompoundUsagePolicy)
   314  
   315  	return nil
   316  }
   317  
   318  func hasUnaryPlusOperator(simpleExpression string) bool {
   319  	getLogger().Enter()
   320  	defer getLogger().Exit()
   321  	return strings.HasSuffix(simpleExpression, PLUS_OPERATOR)
   322  }