Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate add-on SBOM website pages on release #1039

Merged
merged 1 commit into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.zaproxy.gradle.GenerateReleaseStateLastCommit
import org.zaproxy.gradle.GenerateWebsiteAddonsData
import org.zaproxy.gradle.GenerateWebsiteMainReleaseData
import org.zaproxy.gradle.GenerateWebsitePages
import org.zaproxy.gradle.GenerateWebsiteSbomPages
import org.zaproxy.gradle.GenerateWebsiteWeeklyReleaseData
import org.zaproxy.gradle.GitHubRepo
import org.zaproxy.gradle.GitHubUser
Expand Down Expand Up @@ -293,6 +294,12 @@ val generateWebsitePages by tasks.registering(GenerateWebsitePages::class) {
outputDir.set(file("$buildDir/websiteHelpPages"))
}

val generateWebsiteSbomPages by tasks.registering(GenerateWebsiteSbomPages::class) {
releaseState.set(releaseStateData)
zapVersions.set(latestZapVersions)
outputDir.set(file("$buildDir/websiteSbomPages"))
}

val updateZapVersionWebsiteData by tasks.registering(UpdateZapVersionWebsiteData::class) {
releaseState.set(releaseStateData)
val downloadDir = "$siteDir/data/download"
Expand All @@ -312,6 +319,7 @@ val copyWebsiteGeneratedData by tasks.registering(Copy::class) {
}
into("content") {
from(generateWebsitePages)
from(generateWebsiteSbomPages)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2023 The ZAP Development Team
*
* 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.
*/
package org.zaproxy.gradle;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.gradle.api.DefaultTask;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.RegularFileProperty;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;
import org.zaproxy.gradle.website.WebsiteSbomPageGenerator;
import org.zaproxy.zap.utils.ZapXmlConfiguration;

public abstract class GenerateWebsiteSbomPages extends DefaultTask {

private static final String ADDON_ELEMENT_PREFIX = "addon_";
private static final String URL_ELEMENT = ".url";
private static final String NAME_ELEMENT = ".name";

@InputFile
public abstract RegularFileProperty getReleaseState();

@InputFile
public abstract RegularFileProperty getZapVersions();

@OutputDirectory
public abstract DirectoryProperty getOutputDir();

@TaskAction
public void generate() throws Exception {
ReleaseState releaseState = ReleaseState.read(getReleaseState().getAsFile().get());
List<ReleaseState.AddOnChange> addOns = releaseState.getAddOns();
if (addOns == null || addOns.isEmpty()) {
return;
}

Path outputDir = getOutputDir().getAsFile().get().toPath();
ZapXmlConfiguration zapVersions =
new ZapXmlConfiguration(getZapVersions().getAsFile().get());

for (ReleaseState.AddOnChange addOn : addOns) {
if (!addOn.isNewVersion()) {
continue;
}
String addOnId = addOn.getId();
String url = getString(zapVersions, addOnId, URL_ELEMENT);
if (!url.startsWith("https://github.com/zaproxy/zap-extensions")) {
continue;
}

String bomUrl = url.substring(0, url.lastIndexOf("/")) + "/bom.json";
Path bomPath = getTemporaryDir().toPath().resolve(addOnId + ".cdx.json");
TaskUtils.downloadFile(this, bomUrl, bomPath);

String pageTitle = getString(zapVersions, addOnId, NAME_ELEMENT) + " Add-on SBOM";
Path bomPageOutputPath = outputDir.resolve(Path.of("docs", "sbom", addOnId + ".md"));
Files.createDirectories(bomPageOutputPath.getParent());
WebsiteSbomPageGenerator.generate(
bomPath,
bomUrl,
pageTitle,
addOnId,
addOn.getCurrentVersion(),
outputDir.resolve(bomPageOutputPath));
}
}

private static String getString(
ZapXmlConfiguration zapVersions, String addOnId, String element) {
return zapVersions.getString(ADDON_ELEMENT_PREFIX + addOnId + element);
}
}
19 changes: 11 additions & 8 deletions buildSrc/src/main/java/org/zaproxy/gradle/TaskUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,25 +45,28 @@ static Path downloadAddOn(Task task, String urlString) throws IOException {
}

static Path downloadAddOn(Task task, String urlString, Path outputDir) throws IOException {
return downloadFile(task, urlString, outputDir.resolve(extractFileName(urlString)));
}

static Path downloadFile(Task task, String urlString, Path outputFile) throws IOException {
URL url = new URL(urlString);
if (!HTTPS_SCHEME.equalsIgnoreCase(url.getProtocol())) {
throw new IllegalArgumentException(
"The provided URL does not use HTTPS scheme: " + url.getProtocol());
}

Path addOn = outputDir.resolve(extractFileName(urlString));
if (Files.exists(addOn)) {
task.getLogger().info("Add-on already exists, skipping download.");
return addOn;
if (Files.exists(outputFile)) {
task.getLogger().info("File already exists at specified path, skipping download.");
return outputFile;
}

try (InputStream in = url.openStream()) {
Files.copy(in, addOn);
Files.copy(in, outputFile);
} catch (IOException e) {
throw new IOException("Failed to download the add-on: " + e.getMessage(), e);
throw new IOException("Failed to download the file: " + e.getMessage(), e);
}
task.getLogger().info("Add-on downloaded to: " + addOn);
return addOn;
task.getLogger().info("File downloaded to: " + outputFile);
return outputFile;
}

private static String extractFileName(String url) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class PageFrontMatter {
private final int weight;

private AddOnData addOnData;
private SbomData sbomData;

PageFrontMatter(String type, String title, int weight) {
this(type, null, title, weight, null);
Expand All @@ -74,6 +75,14 @@ void setAddOnData(AddOnData addOnData) {
this.addOnData = addOnData;
}

SbomData getSbomData() {
return sbomData;
}

void setSbomData(SbomData sbomData) {
this.sbomData = sbomData;
}

void writeTo(String notice, Writer writer) {
processIoAction(
() -> {
Expand Down Expand Up @@ -102,6 +111,55 @@ String getVersion() {
}
}

static class SbomData {
private final String bomFormat;
private final String downloadUrl;
private final List<SbomDataComponent> components;

SbomData(String bomFormat, String downloadUrl, List<SbomDataComponent> components) {
this.bomFormat = bomFormat;
this.downloadUrl = downloadUrl;
this.components = components;
}

public String getBomFormat() {
return bomFormat;
}

public String getDownloadUrl() {
return downloadUrl;
}

public List<SbomDataComponent> getComponents() {
return components;
}
}

static class SbomDataComponent {
private final String name;
private final String version;
private final String licenses;

SbomDataComponent(String name, String version, String licenses) {
super();
this.name = name;
this.version = version;
this.licenses = licenses;
}

public String getName() {
return name;
}

public String getVersion() {
return version;
}

public String getLicenses() {
return licenses;
}
}

private static class PageFrontMatterRepresenter extends StandardRepresenter {

PageFrontMatterRepresenter(DumpSettings settings) {
Expand Down Expand Up @@ -142,6 +200,27 @@ public Node representData(Object data) {
string("cascade"),
PageFrontMatterRepresenter.this.representData(cascade)));
}

PageFrontMatter.SbomData sbomData = frontMatter.getSbomData();
if (sbomData != null) {
Map<String, Object> sbom = new LinkedHashMap<>();
sbom.put("format", sbomData.getBomFormat());
sbom.put("downloadUrl", sbomData.getDownloadUrl());
List<Map<String, String>> components = new ArrayList<>();
for (SbomDataComponent component : sbomData.getComponents()) {
Map<String, String> componentsMap = new LinkedHashMap<>();
componentsMap.put("name", component.getName());
componentsMap.put("version", component.getVersion());
componentsMap.put("licenses", component.getLicenses());
components.add(componentsMap);
}
sbom.put("components", components);
pageData.add(
new NodeTuple(
string("sbom"),
PageFrontMatterRepresenter.this.representData(sbom)));
}

return node;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2023 The ZAP Development Team
*
* 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.
*/
package org.zaproxy.gradle.website;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

public class WebsiteSbomPageGenerator {

private static final ObjectMapper MAPPER = new ObjectMapper();
private static final String NOTICE =
"This page was automatically generated from the add-on's SBOM.";

public static void generate(
Path bomPath,
String bomUrl,
String pageTitle,
String addOnId,
String addOnVersion,
Path outputFile)
throws Exception {
PageFrontMatter frontMatter = new PageFrontMatter("sbom", pageTitle, 1);
JsonNode bomJson = MAPPER.readTree(bomPath.toFile());
List<PageFrontMatter.SbomDataComponent> resultComponents = new ArrayList<>();
var componentsJsonArray = (ArrayNode) bomJson.get("components");
List<JsonNode> sortedComponentsList =
StreamSupport.stream(componentsJsonArray.spliterator(), false)
.sorted(Comparator.comparing(jsonNode -> jsonNode.get("name").asText()))
.collect(Collectors.toList());
for (JsonNode component : sortedComponentsList) {
var licenses = (ArrayNode) component.get("licenses");
String licensesStr =
StreamSupport.stream(licenses.spliterator(), false)
.map(l -> l.get("license"))
.map(
l ->
l.has("id")
? l.get("id").asText()
: l.has("name") ? l.get("name").asText() : "")
.collect(Collectors.joining(", "));
resultComponents.add(
new PageFrontMatter.SbomDataComponent(
component.get("name").asText(),
component.get("version").asText(),
licensesStr));
}
frontMatter.setSbomData(
new PageFrontMatter.SbomData(
bomJson.get("bomFormat").asText(), bomUrl, resultComponents));
frontMatter.setAddOnData(new PageFrontMatter.AddOnData(addOnId, addOnVersion));
var writer = new StringWriter();
frontMatter.writeTo(NOTICE, writer);
Files.write(outputFile, writer.toString().getBytes(StandardCharsets.UTF_8));
}
}