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 }