Skip to content

Commit 4047c00

Browse files
mhalbritterphilwebb
authored andcommitted
Implement SBOM actuator endpoint
Closes gh-39799
1 parent 75012c5 commit 4047c00

File tree

30 files changed

+22016
-1
lines changed

30 files changed

+22016
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
[[sbom]]
2+
= Software Bill of Materials (`sbom`)
3+
4+
The `sbom` endpoint provides information about the software bill of materials (SBOM).
5+
6+
7+
8+
[[sbom.retrieving-available-sboms]]
9+
== Retrieving the available SBOMs
10+
11+
To retrieve the available SBOMs, make a `GET` request to `/actuator/sbom`, as shown in the following curl-based example:
12+
13+
include::partial$rest/actuator/sbom/curl-request.adoc[]
14+
15+
The resulting response is similar to the following:
16+
17+
include::partial$rest/actuator/sbom/http-response.adoc[]
18+
19+
20+
21+
[[sbom.retrieving-available-sboms.response-structure]]
22+
=== Response Structure
23+
24+
The response contains the available SBOMs.
25+
The following table describes the structure of the response:
26+
27+
[cols="2,1,3"]
28+
include::partial$rest/actuator/sbom/response-fields.adoc[]
29+
30+
31+
32+
[[sbom.retrieving-single-sbom]]
33+
== Retrieving a single SBOM
34+
35+
To retrieve the available SBOMs, make a `GET` request to `/actuator/sbom/\{id}`, as shown in the following curl-based example:
36+
37+
include::partial$rest/actuator/sbom/id/curl-request.adoc[]
38+
39+
The preceding example retrieves the SBOM named application.
40+
The resulting response depends on the format of the SBOM.
41+
This example uses the CycloneDX format.
42+
43+
[source,http,options="nowrap"]
44+
----
45+
HTTP/1.1 200 OK
46+
Content-Type: application/vnd.cyclonedx+json
47+
Accept-Ranges: bytes
48+
Content-Length: 160316
49+
50+
{
51+
"bomFormat" : "CycloneDX",
52+
"specVersion" : "1.5",
53+
"serialNumber" : "urn:uuid:13862013-3360-43e5-8055-3645aa43c548",
54+
"version" : 1,
55+
// ...
56+
}
57+
----
58+
59+
60+
61+
[[sbom.retrieving-single-sbom.response-structure]]
62+
=== Response Structure
63+
The response depends on the format of the SBOM:
64+
65+
* https://cyclonedx.org/specification/overview/[CycloneDX]
66+

spring-boot-project/spring-boot-actuator-autoconfigure/src/docs/antora/modules/api/partials/nav-actuator-rest-api.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
** xref:api:rest/actuator/metrics.adoc[]
1919
** xref:api:rest/actuator/prometheus.adoc[]
2020
** xref:api:rest/actuator/quartz.adoc[]
21+
** xref:api:rest/actuator/sbom.adoc[]
2122
** xref:api:rest/actuator/scheduledtasks.adoc[]
2223
** xref:api:rest/actuator/sessions.adoc[]
2324
** xref:api:rest/actuator/shutdown.adoc[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.sbom;
18+
19+
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
20+
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
21+
import org.springframework.boot.actuate.sbom.SbomEndpoint;
22+
import org.springframework.boot.actuate.sbom.SbomEndpointWebExtension;
23+
import org.springframework.boot.actuate.sbom.SbomProperties;
24+
import org.springframework.boot.autoconfigure.AutoConfiguration;
25+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
26+
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
27+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
28+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
29+
import org.springframework.context.annotation.Bean;
30+
import org.springframework.core.io.ResourceLoader;
31+
32+
/**
33+
* {@link EnableAutoConfiguration Auto-configuration} for {@link SbomEndpoint}.
34+
*
35+
* @author Moritz Halbritter
36+
* @since 3.3.0
37+
*/
38+
@AutoConfiguration
39+
@ConditionalOnAvailableEndpoint(endpoint = SbomEndpoint.class)
40+
@EnableConfigurationProperties(SbomProperties.class)
41+
public class SbomEndpointAutoConfiguration {
42+
43+
private final SbomProperties properties;
44+
45+
SbomEndpointAutoConfiguration(SbomProperties properties) {
46+
this.properties = properties;
47+
}
48+
49+
@Bean
50+
@ConditionalOnMissingBean
51+
SbomEndpoint sbomEndpoint(ResourceLoader resourceLoader) {
52+
return new SbomEndpoint(this.properties, resourceLoader);
53+
}
54+
55+
@Bean
56+
@ConditionalOnMissingBean
57+
@ConditionalOnBean(SbomEndpoint.class)
58+
@ConditionalOnAvailableEndpoint(exposure = EndpointExposure.WEB)
59+
SbomEndpointWebExtension sbomEndpointWebExtension(SbomEndpoint sbomEndpoint) {
60+
return new SbomEndpointWebExtension(sbomEndpoint, this.properties);
61+
}
62+
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/**
18+
* Auto-configuration for actuator SBOM concerns.
19+
*/
20+
package org.springframework.boot.actuate.autoconfigure.sbom;

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ org.springframework.boot.actuate.autoconfigure.r2dbc.ConnectionFactoryHealthCont
9595
org.springframework.boot.actuate.autoconfigure.r2dbc.R2dbcObservationAutoConfiguration
9696
org.springframework.boot.actuate.autoconfigure.data.redis.RedisHealthContributorAutoConfiguration
9797
org.springframework.boot.actuate.autoconfigure.data.redis.RedisReactiveHealthContributorAutoConfiguration
98+
org.springframework.boot.actuate.autoconfigure.sbom.SbomEndpointAutoConfiguration
9899
org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksEndpointAutoConfiguration
99100
org.springframework.boot.actuate.autoconfigure.scheduling.ScheduledTasksObservabilityAutoConfiguration
100101
org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.endpoint.web.documentation;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.boot.actuate.sbom.SbomEndpoint;
22+
import org.springframework.boot.actuate.sbom.SbomEndpointWebExtension;
23+
import org.springframework.boot.actuate.sbom.SbomProperties;
24+
import org.springframework.context.annotation.Bean;
25+
import org.springframework.context.annotation.Configuration;
26+
import org.springframework.context.annotation.Import;
27+
import org.springframework.core.io.ResourceLoader;
28+
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
29+
30+
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
31+
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
32+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
33+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
34+
35+
/**
36+
* Tests for generating documentation describing the {@link SbomEndpoint}.
37+
*
38+
* @author Moritz Halbritter
39+
*/
40+
class SbomEndpointDocumentationTests extends MockMvcEndpointDocumentationTests {
41+
42+
@Test
43+
void sbom() throws Exception {
44+
this.mockMvc.perform(get("/actuator/sbom"))
45+
.andExpect(status().isOk())
46+
.andDo(MockMvcRestDocumentation.document("sbom",
47+
responseFields(fieldWithPath("ids").description("An array of available SBOM ids."))));
48+
}
49+
50+
@Test
51+
void sboms() throws Exception {
52+
this.mockMvc.perform(get("/actuator/sbom/application"))
53+
.andExpect(status().isOk())
54+
.andDo(MockMvcRestDocumentation.document("sbom/id"));
55+
}
56+
57+
@Configuration(proxyBeanMethods = false)
58+
@Import(BaseDocumentationConfiguration.class)
59+
static class TestConfiguration {
60+
61+
@Bean
62+
SbomProperties sbomProperties() {
63+
SbomProperties properties = new SbomProperties();
64+
properties.getApplication().setLocation("classpath:sbom/cyclonedx.json");
65+
return properties;
66+
}
67+
68+
@Bean
69+
SbomEndpoint endpoint(SbomProperties properties, ResourceLoader resourceLoader) {
70+
return new SbomEndpoint(properties, resourceLoader);
71+
}
72+
73+
@Bean
74+
SbomEndpointWebExtension sbomEndpointWebExtension(SbomEndpoint endpoint, SbomProperties properties) {
75+
return new SbomEndpointWebExtension(endpoint, properties);
76+
}
77+
78+
}
79+
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright 2012-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.sbom;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.boot.actuate.sbom.SbomEndpoint;
22+
import org.springframework.boot.actuate.sbom.SbomEndpointWebExtension;
23+
import org.springframework.boot.autoconfigure.AutoConfigurations;
24+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
25+
26+
import static org.assertj.core.api.Assertions.assertThat;
27+
28+
/**
29+
* Tests for {@link SbomEndpointAutoConfiguration}.
30+
*
31+
* @author Moritz Halbritter
32+
*/
33+
class SbomEndpointAutoConfigurationTests {
34+
35+
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
36+
.withConfiguration(AutoConfigurations.of(SbomEndpointAutoConfiguration.class));
37+
38+
@Test
39+
void runShouldHaveEndpointBean() {
40+
this.contextRunner.withPropertyValues("management.endpoints.web.exposure.include=sbom")
41+
.run((context) -> assertThat(context).hasSingleBean(SbomEndpoint.class));
42+
}
43+
44+
@Test
45+
void runWhenNotExposedShouldNotHaveEndpointBean() {
46+
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(SbomEndpoint.class));
47+
}
48+
49+
@Test
50+
void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() {
51+
this.contextRunner.withPropertyValues("management.endpoint.sbom.enabled:false")
52+
.withPropertyValues("management.endpoints.web.exposure.include=*")
53+
.run((context) -> assertThat(context).doesNotHaveBean(SbomEndpoint.class));
54+
}
55+
56+
@Test
57+
void runWhenOnlyExposedOverJmxShouldHaveEndpointBeanWithoutWebExtension() {
58+
this.contextRunner
59+
.withPropertyValues("management.endpoints.web.exposure.include=info", "spring.jmx.enabled=true",
60+
"management.endpoints.jmx.exposure.include=sbom")
61+
.run((context) -> assertThat(context).hasSingleBean(SbomEndpoint.class)
62+
.doesNotHaveBean(SbomEndpointWebExtension.class));
63+
}
64+
65+
}

0 commit comments

Comments
 (0)