diff --git a/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java b/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java index fe6f21f2..0c8d1545 100644 --- a/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java +++ b/src/main/java/org/cyclonedx/maven/BaseCycloneDxMojo.java @@ -42,7 +42,10 @@ import org.cyclonedx.model.LifecycleChoice; import org.cyclonedx.model.Lifecycles; import org.cyclonedx.model.Metadata; +import org.cyclonedx.model.OrganizationalContact; +import org.cyclonedx.model.OrganizationalEntity; import org.cyclonedx.model.Property; +import org.cyclonedx.model.organization.PostalAddress; import org.cyclonedx.parsers.JsonParser; import org.cyclonedx.parsers.Parser; import org.cyclonedx.parsers.XmlParser; @@ -57,6 +60,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; @@ -247,6 +251,13 @@ public abstract class BaseCycloneDxMojo extends AbstractMojo { @Parameter private ExternalReference[] externalReferences; + /** + * Manufacturer information for automatic creator information + * @since 2.9.1 + */ + @Parameter(property = "cyclonedx.manufacturer", required = false) + private OrganizationalEntity manufacturer = null; + @org.apache.maven.plugins.annotations.Component private MavenProjectHelper mavenProjectHelper; @@ -351,6 +362,10 @@ public void execute() throws MojoExecutionException { if (detectUnusedForOptionalScope) { metadata.addProperty(newProperty("maven.optional.unused", Boolean.toString(detectUnusedForOptionalScope))); } + + if (hasManufacturerInformation()) { + metadata.setManufacturer(manufacturer); + } } final Component rootComponent = metadata.getComponent(); @@ -362,6 +377,82 @@ public void execute() throws MojoExecutionException { } } + /** + * Check the mojo configuration for the optional manufacturer contents. + * + * @return {@code true} if there is any manufacturer information configured. + */ + boolean hasManufacturerInformation() { + if (manufacturer == null) { + return false; + } + + return isNotNullOrEmpty(manufacturer.getAddress()) || + isNotNullOrEmpty(manufacturer.getName()) || + isNotNullOrEmptyContacts(manufacturer.getContacts()) || + isNotNullOrEmptyString(manufacturer.getUrls()); + } + + /** + * @param text Some text + * @return {@code true} if there is any text + */ + boolean isNotNullOrEmpty(String text) { + return text != null && !text.trim().isEmpty(); + } + + /** + * @param list A list of text + * @return {@code true} if there is any element has a text value + */ + boolean isNotNullOrEmptyString(List list) { + if (list != null && !list.isEmpty()) { + return list.stream().filter(Objects::nonNull).anyMatch(this::isNotNullOrEmpty); + } + return false; + } + + /** + * @param list A list of contacts + * @return {@code true} if there is any contact has something configured + */ + boolean isNotNullOrEmptyContacts(List list) { + if (list != null && !list.isEmpty()) { + return list.stream().filter(Objects::nonNull).anyMatch(this::isNotNullOrEmpty); + + } + return false; + } + + /** + * @param address A postal address entry + * @return {@code true} if there is any postal address information exists + */ + boolean isNotNullOrEmpty(PostalAddress address) { + if (address == null) { + return false; + } + return isNotNullOrEmpty(address.getStreetAddress()) || + isNotNullOrEmpty(address.getCountry()) || + isNotNullOrEmpty(address.getPostalCode()) || + isNotNullOrEmpty(address.getLocality()) || + isNotNullOrEmpty(address.getPostOfficeBoxNumber()) || + isNotNullOrEmpty(address.getRegion()); + } + + /** + * @param contact A contact entry + * @return {@code true} if there is any contact information exists + */ + boolean isNotNullOrEmpty(OrganizationalContact contact) { + if (null == contact) { + return false; + } + return isNotNullOrEmpty(contact.getName()) || + isNotNullOrEmpty(contact.getEmail()) || + isNotNullOrEmpty(contact.getPhone()); + } + private Property newProperty(String name, String value) { Property property = new Property(); property.setName(name); diff --git a/src/site/markdown/manufacturer.md b/src/site/markdown/manufacturer.md new file mode 100644 index 00000000..4a4ad6d0 --- /dev/null +++ b/src/site/markdown/manufacturer.md @@ -0,0 +1,111 @@ +# Manufacturer +Manufacturer is common in BOMs created through automated processes. + +When creating a number of BOMs for several projects within one organization +or company, it is convenient to attach this information at one place. + +This will also conform to upcoming EU regulation that all SBOM files shall + +>At minimum, the product with digital elements shall be accompanied by: +>1. the name, registered trade name or registered trademark of the manufacturer, and the + > postal address, the email address or other digital contact as well as, where + > available, the website at which the manufacturer can be contacted; +>2. the single point of contact where information about vulnerabilities of the product + > with digital elements can be reported and received, and where the manufacturer’s + > policy on coordinated vulnerability disclosure can be found +>3. name and type and any additional information enabling the unique identification + > of the product with digital elements +>4. the intended purpose of the product with digital elements, including the security + > environment provided by the manufacturer, as well as the product’s essential + > functionalities and information about the security properties + +## Configuration +The configuration is optional. If none is specified, the manufacturer information is not visible. +See https://cyclonedx.org/docs/latest/json/#metadata_manufacturer for more information. + +### name +The name of the organization, + +### address +The physical address (location) of the organization. +##### country +The country name or the two-letter ISO 3166-1 country code. +##### region +The region or state in the country. +##### locality +The locality or city within the country. +##### postOfficeBoxNumber +The post office box number. +##### postalCode +The postal code. +##### streetAddress +The street address. + +### url +The URL of the organization. Multiple URLs are allowed. + +# contact +A contact at the organization. Multiple contacts are allowed. + +##### name +The name of a contact +##### email +The email address of the contact. +##### phone +The phone number of the contact. + +## Example of configuration + +```xml + + + + + org.cyclonedx + cyclonedx-maven-plugin + ${cyclonedx-maven-plugin.version} + + + Example Company + https://www.example.com/contact + + + Steve Springett + Steve.Springett@owasp.org + + + Another contact + 1-800-555-1111 + + + + + + + + +``` + +This configuration will add the following to the BOM file (JSON format): +```json + "manufacturer" : { + "name" : "Example Company", + "url" : [ + "https://www.example.com/contact" + ], + "contact" : [ + { + "name" : "Steve Springett", + "email" : "Steve.Springett@owasp.org" + }, + { + "name" : "Another contact", + "phone" : "1-800-555-1111" + } + ] + } +``` + +## Links +- [EU regulation proposal about SBOM generation.](https://www.europarl.europa.eu/doceo/document/TA-9-2024-0130_EN.pdf) + diff --git a/src/site/site.xml b/src/site/site.xml index c094cc27..3e8abdca 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -26,6 +26,7 @@ + diff --git a/src/test/java/org/cyclonedx/maven/BaseCycloneDxMojoTest.java b/src/test/java/org/cyclonedx/maven/BaseCycloneDxMojoTest.java new file mode 100644 index 00000000..a52f1daa --- /dev/null +++ b/src/test/java/org/cyclonedx/maven/BaseCycloneDxMojoTest.java @@ -0,0 +1,191 @@ +package org.cyclonedx.maven; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.apache.maven.plugin.MojoExecutionException; +import org.cyclonedx.model.Component; +import org.cyclonedx.model.Dependency; +import org.cyclonedx.model.OrganizationalContact; +import org.cyclonedx.model.OrganizationalEntity; +import org.cyclonedx.model.organization.PostalAddress; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class BaseCycloneDxMojoTest { + + /** + * Minimal test class that can create an instance of BaseCycloneDxMojo + * so we can do some unit testing. + */ + private static class BaseCycloneDxMojoImpl extends BaseCycloneDxMojo { + + @Override + protected String extractComponentsAndDependencies(Set topLevelComponents, + Map components, Map dependencies) throws MojoExecutionException { + return ""; + } + } + + @Test + @DisplayName("Verify that the default configuration does not have manufacturer information ") + void hasNoManufacturerInformation() { + BaseCycloneDxMojoImpl mojo = new BaseCycloneDxMojoImpl(); + assertFalse(mojo.hasManufacturerInformation()); + } + + @Test + @DisplayName("Verify that the function hasManufacturerInformation works as expected") + void hasManufacturerInformation() throws NoSuchFieldException, IllegalAccessException { + BaseCycloneDxMojoImpl mojo = new BaseCycloneDxMojoImpl(); + OrganizationalEntity manufacturer = new OrganizationalEntity(); + manufacturer.setName("Manufacturer"); + setParentParameter(mojo, "manufacturer", manufacturer); + assertTrue(mojo.hasManufacturerInformation()); + + manufacturer = new OrganizationalEntity(); + setParentParameter(mojo, "manufacturer", manufacturer); + PostalAddress address = new PostalAddress(); + address.setCountry("UK"); + manufacturer.setAddress(address); + assertTrue(mojo.hasManufacturerInformation()); + + manufacturer = new OrganizationalEntity(); + setParentParameter(mojo, "manufacturer", manufacturer); + OrganizationalContact contact = new OrganizationalContact(); + contact.setName("Contact"); + List contacts = new ArrayList<>(); + contacts.add(contact); + manufacturer.setContacts(contacts); + assertTrue(mojo.hasManufacturerInformation()); + + manufacturer = new OrganizationalEntity(); + setParentParameter(mojo, "manufacturer", manufacturer); + List urls = new ArrayList<>(); + urls.add("https://www.owasp.org"); + manufacturer.setUrls(urls); + assertTrue(mojo.hasManufacturerInformation()); + + } + + + + @Test + @DisplayName("Verify that check of String content works") + void isNotNullOrEmpty() { + BaseCycloneDxMojoImpl mojo = new BaseCycloneDxMojoImpl(); + String value = null; + assertFalse(mojo.isNotNullOrEmpty(value)); + value = ""; + assertFalse(mojo.isNotNullOrEmpty(value)); + value = "null"; + assertTrue(mojo.isNotNullOrEmpty(value)); + } + + @Test + @DisplayName("Verify that a list of strings works as expected") + void isNotNullOrEmptyString() { + List list = null; + BaseCycloneDxMojoImpl mojo = new BaseCycloneDxMojoImpl(); + assertFalse(mojo.isNotNullOrEmptyString(list)); + list = new ArrayList<>(); + assertFalse(mojo.isNotNullOrEmptyString(list)); + String value = null; + list.add(value); + assertFalse(mojo.isNotNullOrEmptyString(list)); + list.add(""); + assertFalse(mojo.isNotNullOrEmptyString(list)); + list.add("null"); + assertTrue(mojo.isNotNullOrEmptyString(list)); + } + + @Test + @DisplayName("Verify that a list of contacts works as expected") + void isNotNullOrEmptyContacts() { + BaseCycloneDxMojoImpl mojo = new BaseCycloneDxMojoImpl(); + List list = null; + assertFalse(mojo.isNotNullOrEmptyContacts(list)); + list = new ArrayList<>(); + assertFalse(mojo.isNotNullOrEmptyContacts(list)); + OrganizationalContact contact = new OrganizationalContact(); + contact.setName("Contact"); + list.add(contact); + assertTrue(mojo.isNotNullOrEmptyContacts(list)); + } + + @Test + @DisplayName("Verify that check of address works as expected") + void testIsNotNullOrEmpty() { + BaseCycloneDxMojoImpl mojo = new BaseCycloneDxMojoImpl(); + PostalAddress address = new PostalAddress(); + assertFalse(mojo.isNotNullOrEmpty(address)); + address.setRegion("AL"); + assertTrue(mojo.isNotNullOrEmpty(address)); + + address = new PostalAddress(); + address.setPostOfficeBoxNumber("12345"); + assertTrue(mojo.isNotNullOrEmpty(address)); + + address = new PostalAddress(); + address.setLocality("my locality"); + assertTrue(mojo.isNotNullOrEmpty(address)); + + address = new PostalAddress(); + address.setPostalCode("12345"); + assertTrue(mojo.isNotNullOrEmpty(address)); + + address = new PostalAddress(); + address.setCountry("US"); + assertTrue(mojo.isNotNullOrEmpty(address)); + + address = new PostalAddress(); + address.setStreetAddress("Main street"); + assertTrue(mojo.isNotNullOrEmpty(address)); + } + + @Test + @DisplayName("Verify that test of contact works as expected") + void testIsNotNullOrEmpty1() { + BaseCycloneDxMojoImpl mojo = new BaseCycloneDxMojoImpl(); + OrganizationalContact contact = null; + assertFalse(mojo.isNotNullOrEmpty(contact)); + contact = new OrganizationalContact(); + assertFalse(mojo.isNotNullOrEmpty(contact)); + contact.setPhone("1-555-888-1234"); + assertTrue(mojo.isNotNullOrEmpty(contact)); + + contact = new OrganizationalContact(); + contact.setEmail("info@example.com"); + assertTrue(mojo.isNotNullOrEmpty(contact)); + + contact = new OrganizationalContact(); + contact.setName("Contact"); + assertTrue(mojo.isNotNullOrEmpty(contact)); + } + + /** + * Inject a parameter value to a superclass (even private parameters). + *

example:

+ *
{@code class B extends A;} + *
{@code class A { private Type a; } } + *
+ *
{@code setParentParameter(new B(), "a", new Type()); } + * + * @param cc The class instance + * @param fieldName The field name + * @param value The value + * @throws NoSuchFieldException If the field does not exist + * @throws IllegalAccessException If the value is not able to be modified + */ + public static void setParentParameter(Object cc, String fieldName, Object value) + throws NoSuchFieldException, IllegalAccessException { + Field field = cc.getClass().getSuperclass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(cc, value); + } + +} \ No newline at end of file