Skip to content

Commit

Permalink
"Bom.serialNumber" data model can have values following the alternati…
Browse files Browse the repository at this point in the history
…ve format allowed in CycloneDX XML (#278)


* Fixed
  * "Bom.serialNumber" data model can have values following the alternative format allowed in CycloneDX XML ([#277] via [#278])
  * `\CycloneDX\Core\Serialization\{DOM,JSON}\Normalizers\BomNormalizer::normalize()` now omits invalid/unsupported values for `serialNumber` ([#277] via [#278])
* Changed
  * `\CycloneDX\Core\Models\Bom::setSerialNumber()` no longer throws `\DomainException` when the value is of an unsupported format ([#277] via [#278])  
    This is considered a non-breaking behaviour change, because the corresponding normalizers assure valid data results.
* Added
  * Bom serialNumber generator: `\CycloneDX\Core\Utils\BomUtility::randomSerialNumber()` ([#277] via [#278])

[#277]: #277
[#278]: #278

---------

Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
  • Loading branch information
jkowalleck committed Mar 24, 2023
1 parent 72a5823 commit dc61f08
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 32 deletions.
12 changes: 12 additions & 0 deletions HISTORY.md
Expand Up @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.

## unreleased

* Fixed
* "Bom.serialNumber" data model can have values following the alternative format allowed in CycloneDX XML ([#277] via [#278])
* `\CycloneDX\Core\Serialization\{DOM,JSON}\Normalizers\BomNormalizer::normalize()` now omits invalid/unsupported values for `serialNumber` ([#277] via [#278])
* Changed
* `\CycloneDX\Core\Models\Bom::setSerialNumber()` no longer throws `\DomainException` when the value is of an unsupported format ([#277] via [#278])
This is considered a non-breaking behaviour change, because the corresponding normalizers assure valid data results.
* Added
* Bom serialNumber generator: `\CycloneDX\Core\Utils\BomUtility::randomSerialNumber()` ([#277] via [#278])

[#277]: https://github.com/CycloneDX/cyclonedx-php-library/issues/277
[#278]: https://github.com/CycloneDX/cyclonedx-php-library/pull/278

## 2.0.0 - 2023-03-20

* BREAKING
Expand Down
1 change: 1 addition & 0 deletions phpmd.xml
Expand Up @@ -23,6 +23,7 @@
<property name="exceptions">
<value>
<!-- some classes are assembled of static methods only - these classes represent namespaces -->
CycloneDX\Core\Utils\BomUtil,
CycloneDX\Core\_helpers\Predicate,
CycloneDX\Core\_helpers\SimpleDOM,
CycloneDX\Core\_helpers\XML
Expand Down
28 changes: 6 additions & 22 deletions src/Core/Models/Bom.php
Expand Up @@ -43,7 +43,8 @@ class Bom
* If specified, the serial number MUST conform to RFC-4122.
* Use of serial numbers are RECOMMENDED.
*
* pattern: ^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
* - pattern for XSD: urn:uuid:([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})|(\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\})
* - pattern for JSON: ^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
*
* @psalm-var non-empty-string|null
*/
Expand Down Expand Up @@ -103,36 +104,19 @@ public function getSerialNumber(): ?string
}

/**
* @param string|null $serialNumber an empty value or a valid urn:uuid
*
* @throws DomainException if version is neither empty nor a valid urn:uuid
* Create valid values with {@see \CycloneDX\Core\Utils\BomUtility::randomSerialNumber()}.
*
* @return $this
*/
public function setSerialNumber(?string $serialNumber): static
{
if ('' === $serialNumber) {
$serialNumber = null;
}
if (null !== $serialNumber && !self::isValidSerialNumber($serialNumber)) {
throw new DomainException("Invalid value: $serialNumber");
}
$this->serialNumber = $serialNumber;
$this->serialNumber = '' === $serialNumber
? null
: $serialNumber;

return $this;
}

/**
* @psalm-pure
*/
private static function isValidSerialNumber(string $serialNumber): bool
{
return 1 === preg_match(
'/^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/',
$serialNumber
);
}

public function getComponents(): ComponentRepository
{
return $this->components;
Expand Down
11 changes: 10 additions & 1 deletion src/Core/Serialization/DOM/Normalizers/BomNormalizer.php
Expand Up @@ -52,7 +52,7 @@ public function normalize(Bom $bom): DOMElement
$element,
[
'version' => $bom->getVersion(),
'serialNumber' => $bom->getSerialNumber(),
'serialNumber' => $this->normalizeSerialNumber($bom->getSerialNumber()),
]
);

Expand All @@ -72,6 +72,15 @@ public function normalize(Bom $bom): DOMElement
return $element;
}

private function normalizeSerialNumber(?string $serialNumber): ?string
{
// @TODO have the regex configurable per Spec
return \is_string($serialNumber) &&
1 === preg_match('/^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$|^\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\}$/', $serialNumber)
? $serialNumber
: null;
}

private function normalizeComponents(ComponentRepository $components): DOMElement
{
$factory = $this->getNormalizerFactory();
Expand Down
11 changes: 10 additions & 1 deletion src/Core/Serialization/JSON/Normalizers/BomNormalizer.php
Expand Up @@ -62,7 +62,7 @@ public function normalize(Bom $bom): array
'$schema' => $this->getSchema($specVersion),
'bomFormat' => self::BOM_FORMAT,
'specVersion' => $specVersion->value,
'serialNumber' => $bom->getSerialNumber(),
'serialNumber' => $this->normalizeSerialNumber($bom->getSerialNumber()),
'version' => $bom->getVersion(),
'metadata' => $this->normalizeMetadata($bom->getMetadata()),
'components' => $factory->makeForComponentRepository()->normalize($bom->getComponents()),
Expand All @@ -78,6 +78,15 @@ public function normalize(Bom $bom): array
);
}

private function normalizeSerialNumber(?string $serialNumber): ?string
{
// @TODO have the regex configurable per Spec
return \is_string($serialNumber) &&
1 === preg_match('/^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $serialNumber)
? $serialNumber
: null;
}

private function normalizeMetadata(Metadata $metadata): ?array
{
$factory = $this->getNormalizerFactory();
Expand Down
55 changes: 55 additions & 0 deletions src/Core/Utils/BomUtility.php
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

/*
* This file is part of CycloneDX PHP Library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/

namespace CycloneDX\Core\Utils;

use Exception;

/**
* Utility regarding:
* - {@see \CycloneDX\Core\Models\Bom}.
*
* @author jkowalleck
*/
abstract class BomUtility
{
/**
* @throws Exception if an appropriate source of randomness cannot be found
*/
public static function randomSerialNumber(): string
{
return sprintf(
'urn:uuid:%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
random_int(0, 0xFFFF),
random_int(0, 0xFFFF),
random_int(0, 0xFFFF),
// UUID version 4
random_int(0, 0x0FFF) | 0x4000,
// UUID version 4 variant 1
random_int(0, 0x3FFF) | 0x8000,
random_int(0, 0xFFFF),
random_int(0, 0xFFFF),
random_int(0, 0xFFFF),
);
}
}
8 changes: 0 additions & 8 deletions tests/Core/Models/BomTest.php
Expand Up @@ -80,14 +80,6 @@ public function testSerialNumberEmptyString(Bom $bom): void
self::assertNull($bom->getSerialNumber());
}

#[DependsUsingShallowClone('testSerialNumber')]
public function testSerialNumberEmptyStringInvalidValue(Bom $bom): void
{
$serialNumber = uniqid('invalid-value', true);
$this->expectException(DomainException::class);
$bom->setSerialNumber($serialNumber);
}

// endregion serialNumber setter&getter

// region components setter&getter&modifiers
Expand Down
47 changes: 47 additions & 0 deletions tests/Core/Utils/BomUtilityTest.php
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

/*
* This file is part of CycloneDX PHP Library.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) OWASP Foundation. All Rights Reserved.
*/

namespace CycloneDX\Tests\Core\Utils;

use CycloneDX\Core\Utils\BomUtility;
use Generator;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

#[CoversClass(BomUtility::class)]
class BomUtilityTest extends TestCase
{
#[DataProvider('dpRandomBomSerialNumberHasCorrectFormat')]
public function testRandomSerialNumberHasCorrectFormat(string $pattern): void
{
$serialNumber = BomUtility::randomSerialNumber();
self::assertMatchesRegularExpression($pattern, $serialNumber);
}

public static function dpRandomBomSerialNumberHasCorrectFormat(): Generator
{
yield 'from XSD' => ['/^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$|^\\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\}$/'];
yield 'from JSON schema' => ['/^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/'];
}
}
1 change: 1 addition & 0 deletions tests/_data/BomModelProvider.php
Expand Up @@ -82,6 +82,7 @@ public static function bomPlain(): Generator
yield 'bom plain' => [new Bom()];
yield 'bom plain with version' => [(new Bom())->setVersion(23)];
yield 'bom plain with serialNumber' => [(new Bom())->setSerialNumber('urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79')];
yield 'bom plain with invalid serialNumber' => [(new Bom())->setSerialNumber('foo bar')];
}

/**
Expand Down

0 comments on commit dc61f08

Please sign in to comment.