This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ant-antlibs-cyclonedx.git
The following commit(s) were added to refs/heads/main by this push:
new eb3e778 properly describe what happens when linking external SBOMs
eb3e778 is described below
commit eb3e7788473156a4091eb89632b4d25b19d42e6c
Author: Stefan Bodewig <[email protected]>
AuthorDate: Sat May 16 12:18:59 2026 +0200
properly describe what happens when linking external SBOMs
and make the implementation agree with what I just came up with :-)
---
docs/component.html | 33 ++
src/main/org/apache/ant/cyclonedx/Component.java | 264 +++++++++-------
.../org/apache/ant/cyclonedx/ComponentBomTask.java | 24 +-
src/tests/antunit/component-test.xml | 334 ++++++++++++++++++++-
4 files changed, 532 insertions(+), 123 deletions(-)
diff --git a/docs/component.html b/docs/component.html
index b929173..c5ad4d9 100644
--- a/docs/component.html
+++ b/docs/component.html
@@ -153,6 +153,39 @@ <h4>any
describes. This is required if you want to include hashes for
the component in your SBOM.</p>
+ <h4 id="sbomLink">sbomLink</h4>
+
+ <p>At most one resource can be specified
+ as <code>sbomLink</code>. When present the referenced resource is
+ read as CycloneDX SBOM and:</p>
+
+ <ul>
+ <li><code>type</code>, <code>name</code>, <code>group</code>,
<code>version</code>,
+ <code>purl</code>, <code>bomRef</code>, <code>scope</code>,
+ <code>decription</code>, <code>publisher</code>, <code>copright</code>,
+ <code>mimeType</code> and <code>manufacturer</code> are taken
+ from the SBOM's metadata component unless they are explicitly
+ specified on the component element itself.</li>
+ <li><code>supplier</code> is taken from the SBOM's metadata
+ component unless it is explicitly specified on the component
+ element itself or <code>manufacturerissupplier</code>
+ is <code>true</code>.</li>
+ <li>Tags are merged wiht those of the SBOM's metadata
+ component.</li>
+ <li><code>author</code>s, <code>license</code>s,
<code>exteranlReference</code>s,
+ <code>dependency</code>s and nested <code>components</code>
+ are taken from the SBOM's metadata component if and only if
+ there is no corresponding element in this component
+ element.</li>
+ <li>Hashes of the linked SBOM are ignored completely.</li>
+ <li>Other components specified in the linked SBOM are also added
+ to the SBOM created by the compomentbom task if they are
+ direct dependencies of the current component element. And
+ their dependencies are set to "unknown" as handling of
+ transitive dependencies is beyond the scope of this
+ library.</li>
+ </ul>
+
<h4 id="manufacturer">manufacturer</h4>
<p>At most one nested <a href="organization.html">organization</a>
diff --git a/src/main/org/apache/ant/cyclonedx/Component.java
b/src/main/org/apache/ant/cyclonedx/Component.java
index 6157154..0e123f2 100644
--- a/src/main/org/apache/ant/cyclonedx/Component.java
+++ b/src/main/org/apache/ant/cyclonedx/Component.java
@@ -7,8 +7,10 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.Stack;
import java.util.stream.Collectors;
@@ -45,7 +47,7 @@ import org.cyclonedx.util.BomUtils;
*/
public class Component extends DataType {
private Resource resource;
- private org.cyclonedx.model.Component.Type type =
org.cyclonedx.model.Component.Type.LIBRARY;
+ private org.cyclonedx.model.Component.Type type;
private String name;
private String group;
private String publisher;
@@ -66,7 +68,7 @@ public class Component extends DataType {
private boolean unknownDependencies = false;
private boolean sbomLinkResolved = false;
private List<OrganizationalContact> authors = new ArrayList<>();
- private List<Tag> tags = new ArrayList<>();
+ private Set<String> tags = new HashSet<>();
private List<Property> properties = new ArrayList<>();
private String mimeType;
private Union sbomLink;
@@ -205,9 +207,9 @@ public class Component extends DataType {
/**
* Adds a tag to the component.
*/
- public void addTag(Tag tag) {
+ public void addConfiguredTag(Tag tag) {
checkChildrenAllowed();
- tags.add(tag);
+ tags.add(tag.getTag());
}
/**
@@ -428,67 +430,30 @@ public class Component extends DataType {
dieOnCircularReference();
if (sbomLink != null && !sbomLinkResolved) {
- if (sbomLink.size() != 1) {
- throw new BuildException("sbomLink requires exactly one nested
resource");
+ Bom bom = readLinkedSbom();
+ if (bom.getMetadata() == null) {
+ throw new BuildException("referenced SBOM file lacks
metadata");
}
- Resource sbom = sbomLink.iterator().next();
- try (InputStream data = sbom.getInputStream();
- ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
- byte[] buf = new byte[4096];
- int count = data.read(buf, 0, buf.length);
- while (count >= 0) {
- baos.write(buf, 0, count);
- count = data.read(buf, 0, buf.length);
- }
- byte[] content = baos.toByteArray();
- try {
- Parser parser = BomParserFactory.createParser(content);
- Bom bom = parser.parse(content);
- if (bom.getMetadata() == null) {
- throw new BuildException("referenced SBOM file lacks
metadata");
- }
- org.cyclonedx.model.Component real =
bom.getMetadata().getComponent();
- if (real == null) {
- throw new BuildException("referenced SBOM file lacks
component");
- }
- fillFrom(real);
-
- List<org.cyclonedx.model.Dependency> allDependencies =
bom.getDependencies();
- if (allDependencies != null) {
- setUnknownDependencies(true);
- org.cyclonedx.model.Dependency myDependencies =
allDependencies
- .stream()
- .filter(d -> Objects.equals(d.getRef(),
getBomRef()))
- .findAny()
- .orElse(null);
- if (myDependencies != null &&
myDependencies.getDependencies() != null) {
- setUnknownDependencies(false);
- dependencies.clear();
- dependencies
- .addAll(myDependencies.getDependencies()
- .stream()
- .map(Dependency::from)
- .collect(Collectors.toList()));
- }
- }
-
- List<org.cyclonedx.model.Component> additionalComponents =
bom.getComponents();
- if (additionalComponents != null &&
!areDependenciesUnknown()) {
- List<Component> toReturn = new ArrayList<>();
- for (org.cyclonedx.model.Component c :
additionalComponents) {
- Component dep = from(c);
- if (dependencies.stream().anyMatch(d ->
Objects.equals(dep.getBomRef(), d.getBomRef()))) {
- // we don't want to resolve transitive
dependencies automatically
- dep.setUnknownDependencies(true);
- toReturn.add(dep);
- }
- }
- return toReturn;
+ org.cyclonedx.model.Component real =
bom.getMetadata().getComponent();
+ if (real == null) {
+ throw new BuildException("referenced SBOM file lacks
component");
+ }
+ List<org.cyclonedx.model.Dependency> allDependencies =
bom.getDependencies();
+ fillFrom(real, allDependencies);
+
+ List<org.cyclonedx.model.Component> additionalComponents =
bom.getComponents();
+ if (additionalComponents != null && !areDependenciesUnknown()) {
+ List<Component> toReturn = new ArrayList<>();
+ for (org.cyclonedx.model.Component c : additionalComponents) {
+ Component dep = from(c, Collections.emptyList());
+ if (dependencies.stream().anyMatch(d ->
Objects.equals(dep.getBomRef(), d.getBomRef()))) {
+ // only include "additional components" this component
depends on directly.
+ // we don't want to resolve transitive dependencies
automatically
+ dep.setUnknownDependencies(true);
+ toReturn.add(dep);
}
-
- } catch (ParseException ex) {
- throw new BuildException("failed to parse sbomlink " +
sbom.getName());
}
+ return toReturn;
}
sbomLinkResolved = true;
}
@@ -522,9 +487,10 @@ public class Component extends DataType {
return component;
}
- private static Component from(org.cyclonedx.model.Component real) {
+ private static Component from(org.cyclonedx.model.Component real,
+ List<org.cyclonedx.model.Dependency>
dependencies) {
Component c = new Component();
- c.fillFrom(real);
+ c.fillFrom(real, dependencies);
return c;
}
@@ -548,7 +514,11 @@ public class Component extends DataType {
org.cyclonedx.model.Component component = new
org.cyclonedx.model.Component();
- component.setType(type);
+ if (type != null) {
+ component.setType(type);
+ } else {
+ component.setType(org.cyclonedx.model.Component.Type.LIBRARY);
+ }
component.setName(name);
if (group != null) {
component.setGroup(group);
@@ -585,9 +555,7 @@ public class Component extends DataType {
component.setProperties(properties);
}
if (!tags.isEmpty()) {
- Tags ts = new Tags();
- ts.setTags(tags.stream().map(t ->
t.getTag()).collect(Collectors.toList()));
- component.setTags(ts);
+ component.setTags(new
Tags(tags.stream().sorted().collect(Collectors.toList())));
}
if (!licenses.isEmpty()) {
LicenseChoice lc = new LicenseChoice();
@@ -615,63 +583,110 @@ public class Component extends DataType {
return component;
}
- private void fillFrom(org.cyclonedx.model.Component real) {
- setType(ComponentType.from(real.getType()));
- setName(real.getName());
- setGroup(real.getGroup());
- setVersion(real.getVersion());
- setDescription(real.getDescription());
- setPublisher(real.getPublisher());
- setCopyright(real.getCopyright());
- setMimeType(real.getMimeType());
- setPurl(real.getPurl());
- setBomRef(real.getBomRef());
- if (real.getScope() != null) {
+ private void fillFrom(org.cyclonedx.model.Component real,
+ List<org.cyclonedx.model.Dependency>
allDependencies) {
+ if (type == null) {
+ setType(ComponentType.from(real.getType()));
+ }
+ if (getBomRef() == null) {
+ setBomRef(real.getBomRef());
+ }
+ if (getPurl() == null) {
+ setPurl(real.getPurl());
+ }
+ if (name == null) {
+ setName(real.getName());
+ }
+ if (group == null) {
+ setGroup(real.getGroup());
+ }
+ if (version == null) {
+ setVersion(real.getVersion());
+ }
+ if (scope == null && real.getScope() != null) {
setScope(ComponentScope.from(real.getScope()));
}
- OrganizationalEntity manufacturer = real.getManufacturer();
- if (manufacturer != null) {
- this.manufacturer = Organization.from(manufacturer);
+ // copy isExternal once CycloneDX Core supports it
+ if (description == null) {
+ setDescription(real.getDescription());
}
- OrganizationalEntity supplier = real.getSupplier();
- if (supplier != null) {
- this.supplier = Organization.from(supplier);
+ if (publisher == null) {
+ setPublisher(real.getPublisher());
}
- LicenseChoice licenses = real.getLicenses();
- if (licenses != null) {
- this.licenses.clear();
- this.licenses.addAll(licenses.getLicenses());
+ if (copyright == null) {
+ setCopyright(real.getCopyright());
}
- if (real.getExternalReferences() != null) {
- this.externalReferences.clear();
- this.externalReferences.addAll(real.getExternalReferences());
+ if (mimeType == null) {
+ setMimeType(real.getMimeType());
}
- if (real.getAuthors() != null) {
- authors.clear();
- authors.addAll(real.getAuthors());
+ if (manufacturer == null) {
+ OrganizationalEntity realManufacturer = real.getManufacturer();
+ if (realManufacturer != null) {
+ manufacturer = Organization.from(realManufacturer);
+ }
}
- if (real.getProperties() != null) {
- properties.clear();
- properties.addAll(real.getProperties());
+ if (supplier == null && !manufacturerIsSupplier) {
+ OrganizationalEntity realSupplier = real.getSupplier();
+ if (realSupplier != null) {
+ supplier = Organization.from(realSupplier);
+ }
+ }
+ if (licenses.isEmpty()) {
+ LicenseChoice realLicenses = real.getLicenses();
+ if (realLicenses != null) {
+ licenses.addAll(realLicenses.getLicenses());
+ }
+ }
+ if (externalReferences.isEmpty()) {
+ List<org.cyclonedx.model.ExternalReference> realExternalReferences
=
+ real.getExternalReferences();
+ if (realExternalReferences != null) {
+ externalReferences.addAll(realExternalReferences);
+ }
+ }
+ if (authors.isEmpty()) {
+ List<OrganizationalContact> realAuthors = real.getAuthors();
+ if (realAuthors != null) {
+ authors.addAll(realAuthors);
+ }
+ }
+ if (properties.isEmpty()) {
+ List<Property> realProperties = real.getProperties();
+ if (realProperties != null) {
+ properties.addAll(realProperties);
+ }
}
if (real.getTags() != null && real.getTags().getTags() != null) {
- tags.clear();
- tags.addAll(real.getTags().getTags()
+ tags.addAll(real.getTags().getTags());
+ }
+ if (nestedComponents.isEmpty()) {
+ List<org.cyclonedx.model.Component> realComponents =
real.getComponents();
+ if (realComponents != null) {
+ nestedComponents.addAll(realComponents.stream()
+ .map(c -> Component.from(c,
allDependencies))
+ .collect(Collectors.toList()));
+ }
+ }
+ if (dependencies.isEmpty() && allDependencies != null) {
+ fillDependencies(allDependencies);
+ }
+ }
+
+ private void fillDependencies(List<org.cyclonedx.model.Dependency>
allDependencies) {
+ setUnknownDependencies(true);
+ org.cyclonedx.model.Dependency myDependencies = allDependencies
+ .stream()
+ .filter(d -> Objects.equals(d.getRef(), getBomRef()))
+ .findAny()
+ .orElse(null);
+ if (myDependencies != null && myDependencies.getDependencies() !=
null) {
+ setUnknownDependencies(false);
+ dependencies
+ .addAll(myDependencies.getDependencies()
.stream()
- .map(t -> {
- Tag tag = new Tag();
- tag.addText(t);
- return tag;
- })
+ .map(Dependency::from)
.collect(Collectors.toList()));
}
- if (real.getComponents() != null) {
- nestedComponents.clear();
- nestedComponents.addAll(real.getComponents()
- .stream()
- .map(Component::from)
- .collect(Collectors.toList()));
- }
}
private void addHashes(org.cyclonedx.model.Component component, Version
bomVersion)
@@ -696,6 +711,29 @@ public class Component extends DataType {
component.setHashes(BomUtils.calculateHashes(file, bomVersion));
}
+ private Bom readLinkedSbom() throws IOException {
+ if (sbomLink.size() != 1) {
+ throw new BuildException("sbomLink requires exactly one nested
resource");
+ }
+ Resource sbom = sbomLink.iterator().next();
+ try (InputStream data = sbom.getInputStream();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ byte[] buf = new byte[4096];
+ int count = data.read(buf, 0, buf.length);
+ while (count >= 0) {
+ baos.write(buf, 0, count);
+ count = data.read(buf, 0, buf.length);
+ }
+ byte[] content = baos.toByteArray();
+ try {
+ Parser parser = BomParserFactory.createParser(content);
+ return parser.parse(content);
+ } catch (ParseException ex) {
+ throw new BuildException("failed to parse sbomlink " +
sbom.getName(), ex);
+ }
+ }
+ }
+
/**
* Represents a dependency of a component.
*/
diff --git a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
index 98806e1..91d4d85 100644
--- a/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
+++ b/src/main/org/apache/ant/cyclonedx/ComponentBomTask.java
@@ -142,7 +142,15 @@ public class ComponentBomTask extends Task {
throw new BuildException("nested component element is required");
}
Set<String> knownComponents = new HashSet<>();
- visitAllComponents(c ->
knownComponents.add(getUnversionedCoordinates(c)));
+ List<Component> resolvedComponents = new ArrayList<>();
+ visitAllComponents(c -> {
+ try {
+ resolvedComponents.addAll(c.resolve());
+ } catch (IOException ex) {
+ throw new BuildException("failed to resolve component",
ex);
+ }
+ knownComponents.add(getUnversionedCoordinates(c));
+ });
meta.setComponent(component.toMainCycloneDxComponent(specVersion.getVersion()));
if (useComponentSupplier) {
OrganizationalEntity componentSupplier =
meta.getComponent().getSupplier();
@@ -160,13 +168,14 @@ public class ComponentBomTask extends Task {
bom.setMetadata(meta);
- if (!additionalComponents.isEmpty() || pureFileComponents.size() > 0) {
- List<org.cyclonedx.model.Component> cs = new ArrayList<>();
- List<Component> resolvedComponents = new ArrayList<>();
+ List<org.cyclonedx.model.Component> cs = new ArrayList<>();
+ if (!additionalComponents.isEmpty()) {
for (Component c : additionalComponents) {
- resolvedComponents.addAll(c.resolve());
cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
}
+ }
+
+ if (!resolvedComponents.isEmpty()) {
for (Component c : resolvedComponents) {
String componentKey = getUnversionedCoordinates(c);
if (!knownComponents.contains(componentKey)) {
@@ -174,6 +183,9 @@ public class ComponentBomTask extends Task {
cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
}
}
+ }
+
+ if (pureFileComponents.size() > 0) {
for (Resource r : pureFileComponents) {
Component c = new Component();
c.setProject(getProject());
@@ -182,9 +194,9 @@ public class ComponentBomTask extends Task {
c.setType(ComponentType.from(org.cyclonedx.model.Component.Type.FILE));
cs.add(c.toAdditionalCycloneDxComponent(specVersion.getVersion()));
}
- bom.setComponents(cs);
}
+ bom.setComponents(cs);
addDependencies(bom);
return bom;
diff --git a/src/tests/antunit/component-test.xml
b/src/tests/antunit/component-test.xml
index c4413d9..be612d6 100644
--- a/src/tests/antunit/component-test.xml
+++ b/src/tests/antunit/component-test.xml
@@ -177,9 +177,9 @@
</cdx:componentbom>
</target>
- <target name="testMaximalComponentData">
+ <target name="createMaximalComponentData" depends="setUp">
<checksum property="ant.file.sha256" file="${ant.file}"
algorithm="SHA-256"/>
- <cdx:componentbom outputdirectory="${output}" format="xml"
+ <cdx:componentbom outputdirectory="${output}" format="all"
xmlns:cdx="antlib:org.apache.ant.cyclonedx">
<component
name="testname"
@@ -199,12 +199,18 @@
</supplier>
<license name="My License"/>
<externalReference type="WEBSITE" url="https://example.com/"/>
- <component name="other-test" group="org.example" version="1.1"/>
+ <component name="other-test" group="org.example" version="1.1"
+ unknownDependencies="true"/>
<author name="Author" email="[email protected]"/>
<tag>label</tag>
<property name="foo" value="bar"/>
+ <dependency bomRef="some-dependency"/>
</component>
+ <additionalComponent name="dep" bomRef="some-dependency"/>
</cdx:componentbom>
+ </target>
+
+ <target name="testMaximalComponentData" depends="createMaximalComponentData">
<xmlproperty file="${output}/bom.xml"/>
<au:assertPropertyEquals
xmlns:au="antlib:org.apache.ant.antunit"
@@ -242,6 +248,10 @@
xmlns:au="antlib:org.apache.ant.antunit"
name="bom.metadata.component(bom-ref)"
value="pkg:maven/org.example/[email protected]?type=jar"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.description"
+ value="My Test Library"/>
<au:assertPropertyEquals
xmlns:au="antlib:org.apache.ant.antunit"
name="bom.metadata.component.authors.author.name"
@@ -314,6 +324,14 @@
xmlns:au="antlib:org.apache.ant.antunit"
resource="${output}/bom.xml"
value='<url>https://example.com/</url>'/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.dependencies.dependency(ref)"
+ value="pkg:maven/org.example/[email protected]?type=jar,some-dependency"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.dependencies.dependency.dependency(ref)"
+ value="some-dependency"/>
</target>
<target name="testManufacturerIsNotCopiedToSupplierByDefault">
@@ -474,7 +492,6 @@
xmlns:cdx="antlib:org.apache.ant.cyclonedx">
<component name="foo" bomRef="foo" unknownDependencies="true"/>
</cdx:componentbom>
- <copy todir="/tmp" file="${output}/bom.json"/>
<au:assertResourceContains
xmlns:au="antlib:org.apache.ant.antunit"
resource="${output}/bom.json"
@@ -747,5 +764,314 @@
<component/>
</cdx:component>
</au:expectfailure>
+ <au:expectfailure
+ expectedMessage='You must not specify nested elements when using refid'
+ xmlns:au="antlib:org.apache.ant.antunit">
+ <cdx:component refid="foo"
+ xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+ <sbomLink/>
+ </cdx:component>
+ </au:expectfailure>
+ </target>
+
+ <target name="testSbomLinkUsesDataFromLinkedSbom"
depends="createMaximalComponentData">
+ <cdx:componentbom
+ bomName="merged"
+ outputdirectory="${output}"
+ format="xml"
+ xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+ <component>
+ <sbomLink>
+ <file file="${output}/bom.json"/>
+ </sbomLink>
+ </component>
+ </cdx:componentbom>
+ <xmlproperty file="${output}/merged.xml"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.name"
+ value="testname"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component(type)"
+ value="library"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.group"
+ value="org.example"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.version"
+ value="1.0"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.publisher"
+ value="test publisher"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.copyright"
+ value="Copyright 2026 ACME Com"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component(mime-type)"
+ value="text/plain"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.purl"
+ value="pkg:maven/org.example/[email protected]?type=jar"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component(bom-ref)"
+ value="pkg:maven/org.example/[email protected]?type=jar"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.description"
+ value="My Test Library"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.authors.author.name"
+ value="Author"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.authors.author.email"
+ value="[email protected]"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.manufacturer.name"
+ value="Example"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.manufacturer.url"
+ value="https://example.org/"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.supplier.name"
+ value="Example 2"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.supplier.url"
+ value="https://example.com/"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.licenses.license.name"
+ value="My License"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.tags.tag"
+ value="label"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.properties.property(name)"
+ value="foo"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.properties.property"
+ value="bar"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.components.component.name"
+ value="other-test"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.components.component(type)"
+ value="library"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.components.component.group"
+ value="org.example"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.components.component.version"
+ value="1.1"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.components.component.purl"
+ value="pkg:maven/org.example/[email protected]?type=jar"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.components.component(bom-ref)"
+ value="pkg:maven/org.example/[email protected]?type=jar"/>
+ <au:assertResourceContains
+ xmlns:au="antlib:org.apache.ant.antunit"
+ resource="${output}/bom.xml"
+ value='<url>https://example.com/</url>'/>
+ <!-- some dependency is not here as we explicitly set the its
+ dependencies to unknown. We don't want top open the can of
+ transitive dependency worms -->
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.dependencies.dependency(ref)"
+ value="pkg:maven/org.example/[email protected]?type=jar"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.dependencies.dependency.dependency(ref)"
+ value="some-dependency"/>
+ <!-- ensure hashes are not taken from the linked SBOM -->
+ <au:assertResourceDoesntContain
+ xmlns:au="antlib:org.apache.ant.antunit"
+ resource="${output}/merged.xml"
+ value='<hash alg="SHA-256">${ant.file.sha256}</hash>'/>
+ </target>
+
+ <target
+ name="testDataFromLinkedSbomCanBeOverwerittenOrMerged"
+ depends="createMaximalComponentData">
+ <cdx:componentbom
+ bomName="merged"
+ outputdirectory="${output}"
+ format="xml"
+ xmlns:cdx="antlib:org.apache.ant.cyclonedx">
+ <component
+ type="APPLICATION"
+ name="testname2"
+ group="org.example2"
+ version="2.0"
+ description="My Second Test Library"
+ publisher="test2 publisher"
+ copyright="Copyright 2026 ACME Corp"
+ mimeType="text/xml"
+ >
+ <file file="${ant.file}"/>
+ <manufacturer name="Example 3">
+ <url url="https://example.org/3"/>
+ </manufacturer>
+ <supplier name="Example 4">
+ <url url="https://example.com/4"/>
+ </supplier>
+ <license name="My Other License"/>
+ <externalReference type="WEBSITE" url="https://example.org/site"/>
+ <component name="yet-another-test" group="org.example2"
+ version="1.2" unknownDependencies="true"/>
+ <author name="Author2" email="[email protected]"/>
+ <tag>label2</tag>
+ <property name="xyzzy" value="baz"/>
+ <dependency bomRef="my-own-dependency"/>
+ <sbomLink>
+ <file file="${output}/bom.json"/>
+ </sbomLink>
+ </component>
+ <additionalComponent name="my-own-dependency"
bomRef="my-own-dependency"/>
+ </cdx:componentbom>
+ <xmlproperty file="${output}/merged.xml"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.name"
+ value="testname2"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component(type)"
+ value="application"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.group"
+ value="org.example2"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.version"
+ value="2.0"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.publisher"
+ value="test2 publisher"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.copyright"
+ value="Copyright 2026 ACME Corp"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component(mime-type)"
+ value="text/xml"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.purl"
+ value="pkg:maven/org.example2/[email protected]?type=jar"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component(bom-ref)"
+ value="pkg:maven/org.example2/[email protected]?type=jar"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.description"
+ value="My Second Test Library"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.authors.author.name"
+ value="Author2"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.authors.author.email"
+ value="[email protected]"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.manufacturer.name"
+ value="Example 3"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.manufacturer.url"
+ value="https://example.org/3"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.supplier.name"
+ value="Example 4"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.supplier.url"
+ value="https://example.com/4"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.licenses.license.name"
+ value="My Other License"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.tags.tag"
+ value="label,label2"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.properties.property(name)"
+ value="xyzzy"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.properties.property"
+ value="baz"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.components.component.name"
+ value="yet-another-test"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.components.component(type)"
+ value="library"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.components.component.group"
+ value="org.example2"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.components.component.version"
+ value="1.2"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.components.component.purl"
+ value="pkg:maven/org.example2/[email protected]?type=jar"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.metadata.component.components.component(bom-ref)"
+ value="pkg:maven/org.example2/[email protected]?type=jar"/>
+ <au:assertResourceContains
+ xmlns:au="antlib:org.apache.ant.antunit"
+ resource="${output}/merged.xml"
+ value='<url>https://example.org/site</url>'/>
+ <au:assertResourceContains
+ xmlns:au="antlib:org.apache.ant.antunit"
+ resource="${output}/merged.xml"
+ value='<hash alg="SHA-256">${ant.file.sha256}</hash>'/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.dependencies.dependency(ref)"
+
value="pkg:maven/org.example2/[email protected]?type=jar,my-own-dependency"/>
+ <au:assertPropertyEquals
+ xmlns:au="antlib:org.apache.ant.antunit"
+ name="bom.dependencies.dependency.dependency(ref)"
+ value="my-own-dependency"/>
</target>
</project>