diff --git a/build.gradle.kts b/build.gradle.kts index 1de5d784..20e783de 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 @@ -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" @@ -312,6 +319,7 @@ val copyWebsiteGeneratedData by tasks.registering(Copy::class) { } into("content") { from(generateWebsitePages) + from(generateWebsiteSbomPages) } } diff --git a/buildSrc/src/main/java/org/zaproxy/gradle/GenerateWebsiteSbomPages.java b/buildSrc/src/main/java/org/zaproxy/gradle/GenerateWebsiteSbomPages.java new file mode 100644 index 00000000..c2fd7829 --- /dev/null +++ b/buildSrc/src/main/java/org/zaproxy/gradle/GenerateWebsiteSbomPages.java @@ -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 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); + } +} diff --git a/buildSrc/src/main/java/org/zaproxy/gradle/TaskUtils.java b/buildSrc/src/main/java/org/zaproxy/gradle/TaskUtils.java index 290f9135..d60eae8d 100644 --- a/buildSrc/src/main/java/org/zaproxy/gradle/TaskUtils.java +++ b/buildSrc/src/main/java/org/zaproxy/gradle/TaskUtils.java @@ -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) { diff --git a/buildSrc/src/main/java/org/zaproxy/gradle/website/PageFrontMatter.java b/buildSrc/src/main/java/org/zaproxy/gradle/website/PageFrontMatter.java index fabdffbd..d647a5fc 100644 --- a/buildSrc/src/main/java/org/zaproxy/gradle/website/PageFrontMatter.java +++ b/buildSrc/src/main/java/org/zaproxy/gradle/website/PageFrontMatter.java @@ -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); @@ -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( () -> { @@ -102,6 +111,55 @@ String getVersion() { } } + static class SbomData { + private final String bomFormat; + private final String downloadUrl; + private final List components; + + SbomData(String bomFormat, String downloadUrl, List components) { + this.bomFormat = bomFormat; + this.downloadUrl = downloadUrl; + this.components = components; + } + + public String getBomFormat() { + return bomFormat; + } + + public String getDownloadUrl() { + return downloadUrl; + } + + public List 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) { @@ -142,6 +200,27 @@ public Node representData(Object data) { string("cascade"), PageFrontMatterRepresenter.this.representData(cascade))); } + + PageFrontMatter.SbomData sbomData = frontMatter.getSbomData(); + if (sbomData != null) { + Map sbom = new LinkedHashMap<>(); + sbom.put("format", sbomData.getBomFormat()); + sbom.put("downloadUrl", sbomData.getDownloadUrl()); + List> components = new ArrayList<>(); + for (SbomDataComponent component : sbomData.getComponents()) { + Map 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; } diff --git a/buildSrc/src/main/java/org/zaproxy/gradle/website/WebsiteSbomPageGenerator.java b/buildSrc/src/main/java/org/zaproxy/gradle/website/WebsiteSbomPageGenerator.java new file mode 100644 index 00000000..c12ac741 --- /dev/null +++ b/buildSrc/src/main/java/org/zaproxy/gradle/website/WebsiteSbomPageGenerator.java @@ -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 resultComponents = new ArrayList<>(); + var componentsJsonArray = (ArrayNode) bomJson.get("components"); + List 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)); + } +}