Skip to content

Commit

Permalink
Merge pull request #1039 from ricekot/sbom-website-pages
Browse files Browse the repository at this point in the history
Generate add-on SBOM website pages on release
  • Loading branch information
thc202 authored Oct 12, 2023
2 parents 5e24233 + a5c8680 commit 01c595d
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 8 deletions.
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));
}
}

0 comments on commit 01c595d

Please sign in to comment.