Implement SBOM actuator endpoint
Closes gh-39799
This commit is contained in:
committed by
Phillip Webb
parent
75012c5173
commit
4047c00aa5
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.sbom;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link SbomEndpoint} exposed by Jersey, Spring MVC, and WebFlux
|
||||
* in CycloneDX format.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class SbomEndpointCycloneDxWebIntegrationTests {
|
||||
|
||||
@WebEndpointTest
|
||||
void shouldReturnSbomContent(WebTestClient client) {
|
||||
client.get()
|
||||
.uri("/actuator/sbom/application")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader()
|
||||
.contentType(MediaType.parseMediaType("application/vnd.cyclonedx+json"))
|
||||
.expectBody()
|
||||
.jsonPath("$.bomFormat")
|
||||
.isEqualTo("CycloneDX");
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
SbomProperties sbomProperties() {
|
||||
SbomProperties properties = new SbomProperties();
|
||||
properties.getApplication().setLocation("classpath:sbom/cyclonedx.json");
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SbomEndpoint sbomEndpoint(SbomProperties properties, ResourceLoader resourceLoader) {
|
||||
return new SbomEndpoint(properties, resourceLoader);
|
||||
}
|
||||
|
||||
@Bean
|
||||
SbomEndpointWebExtension sbomEndpointWebExtension(SbomEndpoint sbomEndpoint, SbomProperties properties) {
|
||||
return new SbomEndpointWebExtension(sbomEndpoint, properties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.sbom;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link SbomEndpoint} exposed by Jersey, Spring MVC, and WebFlux
|
||||
* in SPDX format.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class SbomEndpointSpdxWebIntegrationTests {
|
||||
|
||||
@WebEndpointTest
|
||||
void shouldReturnSbomContent(WebTestClient client) {
|
||||
client.get()
|
||||
.uri("/actuator/sbom/application")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader()
|
||||
.contentType(MediaType.parseMediaType("application/spdx+json"))
|
||||
.expectBody()
|
||||
.jsonPath("$.spdxVersion")
|
||||
.isEqualTo("SPDX-2.3");
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
SbomProperties sbomProperties() {
|
||||
SbomProperties properties = new SbomProperties();
|
||||
properties.getApplication().setLocation("classpath:sbom/spdx.json");
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SbomEndpoint sbomEndpoint(SbomProperties properties, ResourceLoader resourceLoader) {
|
||||
return new SbomEndpoint(properties, resourceLoader);
|
||||
}
|
||||
|
||||
@Bean
|
||||
SbomEndpointWebExtension sbomEndpointWebExtension(SbomEndpoint sbomEndpoint, SbomProperties properties) {
|
||||
return new SbomEndpointWebExtension(sbomEndpoint, properties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.sbom;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link SbomEndpoint} exposed by Jersey, Spring MVC, and WebFlux
|
||||
* in Syft format.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class SbomEndpointSyftWebIntegrationTests {
|
||||
|
||||
@WebEndpointTest
|
||||
void shouldReturnSbomContent(WebTestClient client) {
|
||||
client.get()
|
||||
.uri("/actuator/sbom/application")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader()
|
||||
.contentType(MediaType.parseMediaType("application/vnd.syft+json"))
|
||||
.expectBody()
|
||||
.jsonPath("$.descriptor.name")
|
||||
.isEqualTo("syft");
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
SbomProperties sbomProperties() {
|
||||
SbomProperties properties = new SbomProperties();
|
||||
properties.getApplication().setLocation("classpath:sbom/syft.json");
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SbomEndpoint sbomEndpoint(SbomProperties properties, ResourceLoader resourceLoader) {
|
||||
return new SbomEndpoint(properties, resourceLoader);
|
||||
}
|
||||
|
||||
@Bean
|
||||
SbomEndpointWebExtension sbomEndpointWebExtension(SbomEndpoint sbomEndpoint, SbomProperties properties) {
|
||||
return new SbomEndpointWebExtension(sbomEndpoint, properties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.sbom;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.sbom.SbomEndpoint.Sboms;
|
||||
import org.springframework.boot.actuate.sbom.SbomProperties.Sbom;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link SbomEndpoint}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class SbomEndpointTests {
|
||||
|
||||
private SbomProperties properties;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.properties = new SbomProperties();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldListSboms() {
|
||||
this.properties.getApplication().setLocation("classpath:sbom/cyclonedx.json");
|
||||
this.properties.getAdditional().put("alpha", sbom("classpath:sbom/cyclonedx.json"));
|
||||
this.properties.getAdditional().put("beta", sbom("classpath:sbom/cyclonedx.json"));
|
||||
Sboms sboms = createEndpoint().sboms();
|
||||
assertThat(sboms.ids()).containsExactly("alpha", "application", "beta");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailIfDuplicateSbomIdIsRegistered() {
|
||||
// This adds an SBOM with id 'application'
|
||||
this.properties.getApplication().setLocation("classpath:sbom/cyclonedx.json");
|
||||
this.properties.getAdditional().put("application", sbom("classpath:sbom/cyclonedx.json"));
|
||||
assertThatIllegalStateException().isThrownBy(this::createEndpoint)
|
||||
.withMessage("Duplicate SBOM registration with id 'application'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseLocationFromProperties() throws IOException {
|
||||
this.properties.getApplication().setLocation("classpath:sbom/cyclonedx.json");
|
||||
String content = createEndpoint().sbom("application").getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(content).contains("\"bomFormat\" : \"CycloneDX\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailIfNonExistingLocationIsGiven() {
|
||||
this.properties.getApplication().setLocation("classpath:does-not-exist.json");
|
||||
assertThatIllegalStateException().isThrownBy(() -> createEndpoint().sbom("application"))
|
||||
.withMessageContaining("Resource 'classpath:does-not-exist.json' doesn't exist");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotFailIfNonExistingOptionalLocationIsGiven() {
|
||||
this.properties.getApplication().setLocation("optional:classpath:does-not-exist.json");
|
||||
assertThat(createEndpoint().sbom("application")).isNull();
|
||||
}
|
||||
|
||||
private Sbom sbom(String location) {
|
||||
Sbom result = new Sbom();
|
||||
result.setLocation(location);
|
||||
return result;
|
||||
}
|
||||
|
||||
private SbomEndpoint createEndpoint() {
|
||||
return new SbomEndpoint(this.properties, new GenericApplicationContext());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.sbom;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.EnumSource;
|
||||
import org.junit.jupiter.params.provider.EnumSource.Mode;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||
import org.springframework.boot.actuate.sbom.SbomEndpointWebExtension.SbomType;
|
||||
import org.springframework.boot.actuate.sbom.SbomProperties.Sbom;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link SbomEndpointWebExtension}.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class SbomEndpointWebExtensionTests {
|
||||
|
||||
private SbomProperties properties;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.properties = new SbomProperties();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnHttpOk() {
|
||||
this.properties.getApplication().setLocation("classpath:sbom/cyclonedx.json");
|
||||
WebEndpointResponse<Resource> response = createWebExtension().sbom("application");
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNotFoundIfResourceDoesntExist() {
|
||||
WebEndpointResponse<Resource> response = createWebExtension().sbom("application");
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAutoDetectContentTypeForCycloneDx() {
|
||||
this.properties.getApplication().setLocation("classpath:sbom/cyclonedx.json");
|
||||
WebEndpointResponse<Resource> response = createWebExtension().sbom("application");
|
||||
assertThat(response.getContentType()).isEqualTo(MimeType.valueOf("application/vnd.cyclonedx+json"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAutoDetectContentTypeForSpdx() {
|
||||
this.properties.getApplication().setLocation("classpath:sbom/spdx.json");
|
||||
WebEndpointResponse<Resource> response = createWebExtension().sbom("application");
|
||||
assertThat(response.getContentType()).isEqualTo(MimeType.valueOf("application/spdx+json"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAutoDetectContentTypeForSyft() {
|
||||
this.properties.getApplication().setLocation("classpath:sbom/syft.json");
|
||||
WebEndpointResponse<Resource> response = createWebExtension().sbom("application");
|
||||
assertThat(response.getContentType()).isEqualTo(MimeType.valueOf("application/vnd.syft+json"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSupportUnknownFiles() {
|
||||
this.properties.getApplication().setLocation("classpath:git.properties");
|
||||
WebEndpointResponse<Resource> response = createWebExtension().sbom("application");
|
||||
assertThat(response.getContentType()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseContentTypeIfSet() {
|
||||
this.properties.getApplication().setLocation("classpath:sbom/cyclonedx.json");
|
||||
this.properties.getApplication().setMediaType(MimeType.valueOf("text/plain"));
|
||||
WebEndpointResponse<Resource> response = createWebExtension().sbom("application");
|
||||
assertThat(response.getContentType()).isEqualTo(MimeType.valueOf("text/plain"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseContentTypeForAdditionalSbomsIfSet() {
|
||||
this.properties.getAdditional()
|
||||
.put("alpha", sbom("classpath:sbom/cyclonedx.json", MediaType.valueOf("text/plain")));
|
||||
WebEndpointResponse<Resource> response = createWebExtension().sbom("alpha");
|
||||
assertThat(response.getContentType()).isEqualTo(MimeType.valueOf("text/plain"));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(value = SbomType.class, names = "UNKNOWN", mode = Mode.EXCLUDE)
|
||||
void shouldAutodetectFormats(SbomType type) throws IOException {
|
||||
String content = getSbomContent(type);
|
||||
assertThat(type.matches(content)).isTrue();
|
||||
Arrays.stream(SbomType.values())
|
||||
.filter((candidate) -> candidate != type)
|
||||
.forEach((notType) -> assertThat(notType.matches(content)).isFalse());
|
||||
}
|
||||
|
||||
private String getSbomContent(SbomType type) throws IOException {
|
||||
return switch (type) {
|
||||
case CYCLONE_DX -> readResource("/sbom/cyclonedx.json");
|
||||
case SPDX -> readResource("/sbom/spdx.json");
|
||||
case SYFT -> readResource("/sbom/syft.json");
|
||||
case UNKNOWN -> throw new IllegalArgumentException("UNKNOWN is not supported");
|
||||
};
|
||||
}
|
||||
|
||||
private String readResource(String resource) throws IOException {
|
||||
try (InputStream stream = getClass().getResourceAsStream(resource)) {
|
||||
assertThat(stream).as("Resource '%s'", resource).isNotNull();
|
||||
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
private Sbom sbom(String location, MimeType mediaType) {
|
||||
Sbom sbom = new Sbom();
|
||||
sbom.setLocation(location);
|
||||
sbom.setMediaType(mediaType);
|
||||
return sbom;
|
||||
}
|
||||
|
||||
private SbomEndpointWebExtension createWebExtension() {
|
||||
SbomEndpoint endpoint = new SbomEndpoint(this.properties, new GenericApplicationContext());
|
||||
return new SbomEndpointWebExtension(endpoint, this.properties);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
*
|
||||
* 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
|
||||
*
|
||||
* https://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.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.actuate.sbom;
|
||||
|
||||
import net.minidev.json.JSONArray;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.web.test.WebEndpointTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link SbomEndpoint} exposed by Jersey, Spring MVC, and WebFlux.
|
||||
*
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class SbomEndpointWebIntegrationTests {
|
||||
|
||||
@WebEndpointTest
|
||||
void shouldReturnSboms(WebTestClient client) {
|
||||
client.get()
|
||||
.uri("/actuator/sbom")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader()
|
||||
.contentType(MediaType.parseMediaType("application/vnd.spring-boot.actuator.v3+json"))
|
||||
.expectBody()
|
||||
.jsonPath("$.ids")
|
||||
.value((value) -> assertThat(value).isEqualTo(new JSONArray().appendElement("application")));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
SbomProperties sbomProperties() {
|
||||
SbomProperties properties = new SbomProperties();
|
||||
properties.getApplication().setLocation("classpath:sbom/cyclonedx.json");
|
||||
return properties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
SbomEndpoint sbomEndpoint(SbomProperties properties, ResourceLoader resourceLoader) {
|
||||
return new SbomEndpoint(properties, resourceLoader);
|
||||
}
|
||||
|
||||
@Bean
|
||||
SbomEndpointWebExtension sbomEndpointWebExtension(SbomEndpoint sbomEndpoint, SbomProperties properties) {
|
||||
return new SbomEndpointWebExtension(sbomEndpoint, properties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user