github.com/CycloneDX/sbom-utility@v0.16.0/cmd/patch.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 cmd 20 21 import ( 22 "fmt" 23 "io" 24 "os" 25 "reflect" 26 "strconv" 27 "strings" 28 29 "github.com/CycloneDX/sbom-utility/schema" 30 "github.com/CycloneDX/sbom-utility/utils" 31 "github.com/spf13/cobra" 32 ) 33 34 // flags (do not translate) 35 const ( 36 FLAG_PATCH_FILE = "patch-file" 37 ) 38 39 // flag help (translate) 40 const ( 41 MSG_PATCH_FILE = "patch filename" 42 ) 43 44 const ( 45 ERR_PATCH_REPLACE_PATH_EXISTS = "invalid path. Path does not exist to replace value" 46 ) 47 48 // The "-" character is used to index the end of the array (see [RFC6901]) 49 const ( 50 RFC6901_END_OF_ARRAY = "-" 51 ) 52 53 var PATCH_OUTPUT_SUPPORTED_FORMATS = MSG_SUPPORTED_OUTPUT_FORMATS_HELP + 54 strings.Join([]string{FORMAT_JSON}, ", ") 55 56 // Command PreRunE helper function to test for patch file 57 func preRunTestForPatchFile(args []string) error { 58 getLogger().Enter() 59 defer getLogger().Exit() 60 getLogger().Tracef("args: %v", args) 61 62 // Make sure the input filename is present and exists 63 patchFilename := utils.GlobalFlags.PatchFlags.PatchFile 64 if patchFilename == "" { 65 return getLogger().Errorf("Missing required argument(s): %s", FLAG_PATCH_FILE) 66 } else if _, err := os.Stat(patchFilename); err != nil { 67 return getLogger().Errorf("File not found: `%s`", patchFilename) 68 } 69 return nil 70 } 71 72 func NewCommandPatch() *cobra.Command { 73 var command = new(cobra.Command) 74 command.Use = CMD_USAGE_PATCH 75 command.Short = "Apply an IETF RFC 6902 patch file to a JSON BOM file" 76 command.Long = "Apply an IETF RFC 6902 patch file to a JSON BOM file" 77 command.RunE = patchCmdImpl 78 command.PreRunE = func(cmd *cobra.Command, args []string) (err error) { 79 // Test for required flags (parameters) 80 err = preRunTestForInputFile(args) 81 if err != nil { 82 return 83 } 84 err = preRunTestForPatchFile(args) 85 if err != nil { 86 return 87 } 88 return 89 } 90 initCommandPatchFlags(command) 91 92 return command 93 } 94 95 func initCommandPatchFlags(command *cobra.Command) (err error) { 96 getLogger().Enter() 97 defer getLogger().Exit() 98 99 // NOTE: Cobra commands that use the same variable with different "default" values (i.e., "txt", "json") 100 // will overwrite each other during initialization... we must use a unique variable for each command/conflict 101 command.PersistentFlags().StringVar(&utils.GlobalFlags.PatchFlags.OutputFormat, FLAG_OUTPUT_FORMAT, FORMAT_JSON, 102 MSG_FLAG_OUTPUT_FORMAT+PATCH_OUTPUT_SUPPORTED_FORMATS) 103 command.Flags().StringVarP(&utils.GlobalFlags.PatchFlags.PatchFile, FLAG_PATCH_FILE, "", "", MSG_PATCH_FILE) 104 err = command.MarkFlagRequired(FLAG_PATCH_FILE) 105 if err != nil { 106 err = getLogger().Errorf("unable to mark flag `%s` as required: %s", FLAG_PATCH_FILE, err) 107 } 108 return 109 } 110 111 func patchCmdImpl(cmd *cobra.Command, args []string) (err error) { 112 getLogger().Enter(args) 113 defer getLogger().Exit() 114 115 // Create output writer 116 outputFilename := utils.GlobalFlags.PersistentFlags.OutputFile 117 outputFile, writer, err := createOutputFile(outputFilename) 118 getLogger().Tracef("outputFile: `%v`; writer: `%v`", outputFilename, writer) 119 120 // Overcome Cobra limitation in variable reuse between diff. commands 121 // That is, as soon as ANY command sets a default value, it cannot be changed 122 utils.GlobalFlags.PersistentFlags.OutputFormat = utils.GlobalFlags.PatchFlags.OutputFormat 123 124 // use function closure to assure consistent error output based upon error type 125 defer func() { 126 // always close the output file 127 if outputFile != nil { 128 outputFile.Close() 129 getLogger().Infof("Closed output file: `%s`", outputFilename) 130 } 131 }() 132 133 if err == nil { 134 err = Patch(writer, utils.GlobalFlags.PersistentFlags, utils.GlobalFlags.PatchFlags) 135 } 136 137 return 138 } 139 140 // Assure all errors are logged 141 func processPatchResults(err error) { 142 if err != nil { 143 // No special processing at this time 144 getLogger().Error(err) 145 } 146 } 147 148 // NOTE: resourceType has already been validated 149 func Patch(writer io.Writer, persistentFlags utils.PersistentCommandFlags, patchFlags utils.PatchCommandFlags) (err error) { 150 getLogger().Enter() 151 defer getLogger().Exit() 152 153 // use function closure to assure consistent error output based upon error type 154 defer func() { 155 if err != nil { 156 processPatchResults(err) 157 } 158 }() 159 160 // Note: returns error if either file load or unmarshal to JSON map fails 161 var document *schema.BOM 162 if document, err = LoadInputBOMFileAndDetectSchema(); err != nil { 163 return 164 } 165 166 // At this time, fail SPDX format SBOMs as "unsupported" (for "any" format) 167 if !document.FormatInfo.IsCycloneDx() { 168 err = schema.NewUnsupportedFormatForCommandError( 169 document.FormatInfo.CanonicalName, 170 document.GetFilename(), 171 CMD_LICENSE, FORMAT_ANY) 172 return 173 } 174 175 // validate parameters 176 patchFile := utils.GlobalFlags.PatchFlags.PatchFile 177 if patchFile == "" { 178 err = fmt.Errorf("invalid patch file: %s", patchFile) 179 return 180 } 181 182 patchDocument := NewIETFRFC6902PatchDocument(patchFile) 183 if err = patchDocument.UnmarshalRecords(); err != nil { 184 return 185 } 186 187 if err = processPatchRecords(document, patchDocument); err != nil { 188 return 189 } 190 191 // After patch records are applied to the JSON map; 192 // update the corresponding "CdxBom" using the "unmarshal" wrapper. 193 // NOTE: If any JSON keys that are NOT part of the CycloneDX spec. 194 // have been added via a patch "add" operation, they will be removed 195 // during the unmarshal process. 196 if document.CdxBom, err = schema.UnMarshalDocument(document.JsonMap); err != nil { 197 return 198 } 199 200 // Output the "patched" version of the Input BOM 201 format := persistentFlags.OutputFormat 202 getLogger().Infof("Writing patched BOM (`%s` format)...", format) 203 switch format { 204 case FORMAT_JSON: 205 err = document.WriteAsEncodedJSONInt(writer, utils.GlobalFlags.PersistentFlags.GetOutputIndentInt()) 206 default: 207 // Default to Text output for anything else (set as flag default) 208 getLogger().Warningf("Patch not supported for `%s` format; defaulting to `%s` format...", 209 format, FORMAT_JSON) 210 err = document.WriteAsEncodedJSONInt(writer, utils.GlobalFlags.PersistentFlags.GetOutputIndentInt()) 211 } 212 213 return 214 } 215 216 func innerPatch(document *schema.BOM) (err error) { 217 // validate parameters 218 patchFile := utils.GlobalFlags.PatchFlags.PatchFile 219 if patchFile == "" { 220 err = fmt.Errorf("invalid patch file: %s", patchFile) 221 return 222 } 223 224 patchDocument := NewIETFRFC6902PatchDocument(patchFile) 225 if err = patchDocument.UnmarshalRecords(); err != nil { 226 return 227 } 228 229 if err = processPatchRecords(document, patchDocument); err != nil { 230 return 231 } 232 return 233 } 234 235 func processPatchRecords(bomDocument *schema.BOM, patchDocument *IETF6902Document) (err error) { 236 getLogger().Enter() 237 defer getLogger().Exit() 238 239 for _, record := range patchDocument.Records { 240 getLogger().Tracef("patch: %s\n", record.String()) 241 242 // operation objects MUST have exactly one "path" member. 243 // That member's value is a string containing a JSON-Pointer value 244 // [RFC6901] that references a location within the target document 245 // (the "target location") where the operation is performed. 246 // NOTE: RFC 6901 indicates an "empty" path means a pointer to the 247 // entire document which effectively mean patch the entire document 248 // which does not make sense... 249 if record.Path == "" { 250 // TODO: make this a declared error type that can be tested 251 return fmt.Errorf("invalid IETF RFC 6902 patch operation. \"path\" is empty") 252 } 253 254 var keys []string 255 jsonMap := bomDocument.GetJSONMap() 256 257 if jsonMap == nil { 258 return fmt.Errorf("invalid json document (nil)") 259 } 260 261 if keys, err = parseMapKeysFromPath(record.Path); err != nil { 262 return 263 } 264 265 lengthKeys := len(keys) 266 if lengthKeys == 0 { 267 return fmt.Errorf("invalid document path (nil)") 268 } 269 270 switch record.Operation { 271 case IETF_RFC6902_OP_ADD: 272 if record.Value == nil { 273 // TODO: make this a declared error type that can be tested 274 return fmt.Errorf("invalid IETF RFC 6902 patch operation. \"value\" missing") 275 } 276 if err = addOrReplaceValue(jsonMap, keys, record.Value, false); err != nil { 277 return 278 } 279 case IETF_RFC6902_OP_REPLACE: 280 // NOTE: Replace logic is identical to "add" operation except that 281 // the target "key" MUST exist... 282 if record.Value == nil { 283 // TODO: make this a declared error type that can be tested 284 return fmt.Errorf("invalid IETF RFC 6902 patch operation. \"value\" missing") 285 } 286 if err = addOrReplaceValue(jsonMap, keys, record.Value, true); err != nil { 287 return 288 } 289 case IETF_RFC6902_OP_REMOVE: 290 if err = removeValue(jsonMap, keys, record.Value); err != nil { 291 return 292 } 293 case IETF_RFC6902_OP_TEST: 294 // NOTE: "test" operations do not change the input JSON. They either 295 // will report (via INFO messages) "success" of a data match or return 296 // a (typed) error that indicates a non-match and terminates all 297 // patch record processing. 298 var equal bool 299 var actualValue interface{} 300 if equal, actualValue, err = testValue(jsonMap, keys, record.Value); err != nil { 301 return 302 } 303 // The RFC6902 spec. requires returning an "error" if the test values does not match 304 // the value found in the document... 305 if !equal { 306 err = NewIETFRFC6902TestError(record.String(), actualValue) 307 return 308 } 309 successMessage := fmt.Sprintf("%s. test record: %s, actual value: %v\n", MSG_IETF_RFC6902_OPERATION_SUCCESS, record.String(), actualValue) 310 getLogger().Info(successMessage) 311 case IETF_RFC6902_OP_MOVE: 312 fallthrough 313 case IETF_RFC6902_OP_COPY: 314 return NewUnsupportedError(record.Operation, "IETF RFC 6902 operation not currently supported") 315 default: 316 return NewUnsupportedError(record.Operation, "invalid IETF RFC 6902 operation") 317 } 318 } 319 320 return 321 } 322 323 func parseMapKeysFromPath(path string) (keys []string, err error) { 324 // first char SHOULD be a forward slash, if not error 325 if path == "" || path[0] != '/' { 326 err = fmt.Errorf("invalid path. Path must begin with forward slash") 327 return 328 } 329 // parse out paths ignoring leading forward slash character 330 keys = strings.Split(path[1:], "/") 331 return 332 } 333 334 func parseArrayIndex(indexPath string) (arrayIndex int, err error) { 335 // Check for RFC6901 end-of-array character 336 if indexPath == RFC6901_END_OF_ARRAY { 337 arrayIndex = -1 338 return 339 } 340 // otherwise, the path should be convertible to an integer 341 arrayIndex, err = strconv.Atoi(indexPath) 342 return 343 } 344 345 // func parseArrayIndexFromPath(path string) (arrayIndex int, err error) { 346 // var keys []string 347 // keys, err = parseMapKeysFromPath(path) 348 // if err != nil { 349 // return 350 // } 351 352 // lengthKeys := len(keys) 353 // if lengthKeys <= 0 { 354 // err = fmt.Errorf("invalid path. Path: %s", path) 355 // return 356 // } 357 // return parseArrayIndex(keys[lengthKeys-1]) 358 // } 359 360 // The "test" operation tests that a value at the target location is 361 // equal to a specified value. 362 // - The operation object MUST contain a "value" member that conveys the 363 // value to be compared to the target location's value. 364 // - The target location MUST be equal to the "value" value for the 365 // operation to be considered successful. 366 // 367 // Here, "equal" means that the value at the target location and the 368 // value conveyed by "value" are of the same JSON type, and that they 369 // are considered equal by the following rules for that type: 370 // 371 // - strings: are considered equal if they contain the same number of 372 // Unicode characters and their code points are byte-by-byte equal. 373 // 374 // - numbers: are considered equal if their values are numerically 375 // equal. 376 // 377 // - arrays: are considered equal if they contain the same number of 378 // 379 // values, and if each value can be considered equal to the value at 380 // the corresponding position in the other array, using this list of 381 // type-specific rules. 382 // 383 // - objects: are considered equal if they contain the same number of 384 // members, and if each member can be considered equal to a member in 385 // the other object, by comparing their keys (as strings) and their 386 // values (using this list of type-specific rules). 387 // 388 // - literals (false, true, and null): are considered equal if they are 389 // the same. 390 func testValue(parentMap map[string]interface{}, keys []string, value interface{}) (equal bool, actualValue interface{}, err error) { 391 var nextNodeKey string // := keys[0] 392 var nextNode interface{} // := parentMap[nextNodeKey] 393 lengthKeys := len(keys) 394 395 switch lengthKeys { 396 case 0: 397 err = fmt.Errorf("invalid map key (nil)") 398 return 399 case 1: // special case of adding new key/value to document root 400 nextNode = parentMap 401 default: // adding keys/values along document path 402 nextNodeKey = keys[0] 403 nextNode = parentMap[nextNodeKey] 404 } 405 406 switch typedNode := nextNode.(type) { 407 case map[string]interface{}: 408 // If the resulting value is indeed another map type, we expect for a Json Map 409 // we preserve that pointer for the next iteration 410 if lengthKeys > 2 { 411 // if the next node is a map AND there is more than one path following it, 412 // it would mean we have not yet reached the final map or slice to add 413 // a value to 414 equal, actualValue, err = testValue(typedNode, keys[1:], value) 415 return 416 } else { 417 // if the next node is a map AND only 1 path remains after it, 418 // it would mean that last path is a new key to be added 419 // to the next node's map with the provided value 420 actualValue = typedNode[keys[0]] 421 equal, err = isValueEqual(value, actualValue) 422 if !equal || err != nil { 423 return 424 } 425 } 426 case []interface{}: 427 if lengthKeys != 2 { 428 // TODO: create a formal error type for this 429 err = fmt.Errorf("invalid path. IETF RFC 6901 does not permit paths after array indices") 430 return 431 } 432 var arrayIndex int 433 indexPath := keys[1] 434 arrayIndex, err = parseArrayIndex(indexPath) 435 if err != nil { 436 return 437 } 438 actualValue = typedNode[arrayIndex] 439 equal, err = isValueEqual(value, actualValue) 440 if !equal || err != nil { 441 return 442 } 443 default: 444 // Optimistically, assign the value and emit a warning of the unexpected JSON type 445 getLogger().Warningf("Invalid document node type: (%T)", nextNode) 446 return 447 } 448 return 449 } 450 451 func isValueEqual(value1 interface{}, value2 interface{}) (equal bool, err error) { 452 // We want to assure type match before actual value comparison 453 switch value1.(type) { 454 case bool: 455 if _, ok := value2.(bool); !ok { 456 err = fmt.Errorf("invalid type comparison. value1: %v (%T), value2: %v (%T)", value1, value1, value2, value2) 457 return 458 } 459 equal = (value1 == value2) 460 case float64: 461 if _, ok := value2.(float64); !ok { 462 err = fmt.Errorf("invalid type comparison. value1: %v (%T), value2: %v (%T)", value1, value1, value2, value2) 463 return 464 } 465 equal = (value1 == value2) 466 case string: 467 if _, ok := value2.(string); !ok { 468 err = fmt.Errorf("invalid type comparison. value1: %v (%T), value2: %v (%T)", value1, value1, value2, value2) 469 return 470 } 471 equal = (value1 == value2) 472 return 473 case []interface{}: 474 if _, ok := value2.([]interface{}); !ok { 475 err = fmt.Errorf("invalid type comparison. value1: %v (%T), value2: %v (%T)", value1, value1, value2, value2) 476 return 477 } 478 equal = reflect.DeepEqual(value1, value2) 479 return 480 case map[string]interface{}: 481 if _, ok := value2.(map[string]interface{}); !ok { 482 err = fmt.Errorf("invalid type comparison. value1: %v (%T), value2: %v (%T)", value1, value1, value2, value2) 483 return 484 } 485 equal = reflect.DeepEqual(value1, value2) 486 return 487 default: 488 err = fmt.Errorf("invalid type comparison. Unexpected type for value: %v (%T)", value1, value1) 489 return 490 } 491 492 return 493 } 494 495 // The "remove" operation removes the value at the target location. 496 // 497 // - The target location MUST exist for the operation to be successful. 498 // - If removing an element from an array, any elements above the 499 // specified index are shifted one position to the left. 500 func removeValue(parentMap map[string]interface{}, keys []string, value interface{}) (err error) { 501 var nextNodeKey string // := keys[0] 502 var nextNode interface{} // := parentMap[nextNodeKey] 503 lengthKeys := len(keys) 504 505 switch lengthKeys { 506 case 0: 507 return fmt.Errorf("invalid map key (nil)") 508 case 1: // special case of adding new key/value to document root 509 nextNode = parentMap 510 default: // adding keys/values along document path 511 nextNodeKey = keys[0] 512 nextNode = parentMap[nextNodeKey] 513 } 514 515 switch typedNode := nextNode.(type) { 516 case map[string]interface{}: 517 // If the resulting value is indeed another map type, we expect for a Json Map 518 // we preserve that pointer for the next iteration 519 if lengthKeys > 2 { 520 // if the next node is a map AND there is more than one path following it, 521 // it would mean we have not yet reached the final map or slice to add 522 // a value to 523 err = removeValue(typedNode, keys[1:], value) 524 return 525 } else { 526 // if the next node is a map AND only 1 path remains after it, 527 // it would mean that last path is a new key to be added 528 // to the next node's map with the provided value 529 delete(typedNode, keys[0]) 530 } 531 case []interface{}: 532 if lengthKeys != 2 { 533 err = fmt.Errorf("invalid path. IETF RFC 6901 does not permit paths after array indices") 534 return 535 } 536 537 var arrayIndex int 538 indexPath := keys[1] 539 arrayIndex, err = parseArrayIndex(indexPath) 540 if err != nil { 541 return 542 } 543 var newSlice []interface{} 544 newSlice, err = removeValueFromSliceAtIndex(typedNode, arrayIndex) 545 parentMap[nextNodeKey] = newSlice 546 case float64: 547 // NOTE: It is a conscious decision of tbe encoding/json package to 548 // decode all Number values to float64 549 parentMap[nextNodeKey] = value 550 case bool: 551 parentMap[nextNodeKey] = value 552 default: 553 // Optimistically, assign the value and emit a warning of the unexpected JSON type 554 parentMap[nextNodeKey] = value 555 getLogger().Warningf("Invalid document node type: (%T)", nextNode) 556 return 557 } 558 return 559 } 560 561 // The "add" operation performs one of the following functions, 562 // depending upon what the target location references: 563 // 564 // - If the target location specifies an array index, a new value is 565 // inserted into the array at the specified index. 566 // 567 // - If the target location specifies an object member that does not 568 // already exist, a new member is added to the object. 569 // 570 // - If the target location specifies an object member that does exist, 571 // that member's value is replaced. 572 // 573 // The operation object MUST contain a "value" member whose content 574 // specifies the value to be added. 575 // 576 // The "replace" operation replaces the value at the target location 577 // with a new value. The operation object MUST contain a "value" member 578 // whose content specifies the replacement value. 579 func addOrReplaceValue(parentMap map[string]interface{}, keys []string, value interface{}, replace bool) (err error) { 580 var nextNodeKey string // := keys[0] 581 var nextNode interface{} // := parentMap[nextNodeKey] 582 lengthKeys := len(keys) 583 584 switch lengthKeys { 585 case 0: 586 return fmt.Errorf("invalid map key (nil)") 587 case 1: // special case of adding new key/value to document root 588 nextNode = parentMap 589 default: // adding keys/values along document path 590 nextNodeKey = keys[0] 591 nextNode = parentMap[nextNodeKey] 592 } 593 594 switch typedNode := nextNode.(type) { 595 case map[string]interface{}: 596 // If the resulting value is indeed another map type, we expect for a Json Map 597 // we preserve that pointer for the next iteration 598 if lengthKeys > 2 { 599 // if the next node is a map AND there is more than one path following it, 600 // it would mean we have not yet reached the final map or slice to add 601 // a value to 602 err = addOrReplaceValue(typedNode, keys[1:], value, replace) 603 return 604 } else { 605 // if the next node is a map AND only 1 path remains after it, 606 // it would mean that last path is a new key to be added 607 // to the next node's map with the provided value 608 currentKey := keys[lengthKeys-1] 609 if _, exists := typedNode[currentKey]; !exists && replace { 610 err = fmt.Errorf(ERR_PATCH_REPLACE_PATH_EXISTS) 611 return 612 } 613 typedNode[currentKey] = value 614 } 615 case []interface{}: 616 if lengthKeys != 2 { 617 err = fmt.Errorf("invalid path. IETF RFC 6901 does not permit paths after array indices") 618 return 619 } 620 621 var arrayIndex int 622 indexPath := keys[1] 623 arrayIndex, err = parseArrayIndex(indexPath) 624 if err != nil { 625 return 626 } 627 newSlice := insertValueIntoSlice(nextNode.([]interface{}), arrayIndex, value) 628 parentMap[nextNodeKey] = newSlice 629 case float64: 630 // NOTE: It is a conscious decision of tbe encoding/json package to 631 // decode all Number values to float64 632 parentMap[nextNodeKey] = value 633 case bool: 634 parentMap[nextNodeKey] = value 635 default: 636 // Optimistically, assign the value and emit a warning of the unexpected JSON type 637 parentMap[nextNodeKey] = value 638 getLogger().Warningf("Invalid document node type: (%T)", nextNode) 639 return 640 } 641 return 642 } 643 644 func insertValueIntoSlice(slice []interface{}, index int, value interface{}) []interface{} { 645 if index == -1 || index >= len(slice) { 646 return append(slice, value) 647 } 648 slice = append(slice[:index+1], slice[index:]...) 649 slice[index] = value 650 return slice 651 } 652 653 func removeValueFromSliceAtIndex(slice []interface{}, index int) (newSlice []interface{}, err error) { 654 if index < 0 || index >= len(slice) { 655 err = fmt.Errorf("remove array element failed. Index (%v) out of range for array (length: %v). ", index, len(slice)) 656 return 657 } 658 // unpack elements from the slice subsets (i.e. using ... notation) 659 return append(slice[:index], slice[index+1:]...), nil 660 }