├── .github └── dependabot.yml ├── .gitignore ├── Jenkinsfile ├── LICENSE ├── README.MD ├── pom.xml ├── src ├── main │ ├── java │ │ └── io │ │ │ └── projectdiscovery │ │ │ └── plugins │ │ │ └── jenkins │ │ │ └── nuclei │ │ │ ├── CompressionUtil.java │ │ │ ├── NucleiBuilder.java │ │ │ ├── NucleiBuilderHelper.java │ │ │ ├── NucleiDownloadHelper.java │ │ │ ├── SupportedArchitecture.java │ │ │ └── SupportedOperatingSystem.java │ └── resources │ │ ├── index.jelly │ │ └── io │ │ └── projectdiscovery │ │ └── plugins │ │ └── jenkins │ │ └── nuclei │ │ ├── Messages.properties │ │ └── NucleiBuilder │ │ ├── config.jelly │ │ ├── config.properties │ │ ├── help-additionalFlags.html │ │ ├── help-nucleiVersion.html │ │ ├── help-reportingConfiguration.html │ │ └── help-targetUrl.html └── test │ └── java │ └── io │ └── projectdiscovery │ └── plugins │ └── jenkins │ └── nuclei │ └── NucleiBuilderTest.java └── static ├── nuclei-logo.png └── nuclei-plugin.png /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | - package-ecosystem: github-actions 2 | directory: / 3 | schedule: 4 | interval: daily -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | 3 | .idea/ 4 | 5 | *.iml -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin( 2 | useContainerAgent: true, 3 | configurations: [ 4 | [platform: 'linux', jdk: 8], 5 | [platform: 'windows', jdk: 8], 6 | ]) 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 ProjectDiscovery, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 |

2 |
3 | Nuclei 4 |

5 | 6 |

Fast and customisable vulnerability scanner based on simple YAML based DSL.

7 | 8 |

9 | Twitter 10 | Discord 11 |

12 | 13 | # Vulnerability scanning using Nuclei 14 | Jenkins Minimum Version 15 | JDK Minimum Version 16 | License 17 | 18 | ## How it works 19 | * The plugin downloads the latest release of Nuclei from GitHub, based on the build executor's operating system and architecture 20 | * The downloaded artifact is uncompressed 21 | * If the currently executing build already has a Nuclei binary within it's workspace, the first two steps are skipped 22 | * Nuclei Templates are downloaded/updated 23 | * Scan is executed using the provided user-input 24 | 25 | ## Usage 26 | * Create or edit a **Freestyle** project 27 | * Add a **Nuclei Vulnerability Scanner** build step 28 | * Introduce the URL of the target web application you intend to test 29 | * Optionally: 30 | * add reporting configuration that allows automatic issue creation on platforms like Jira and GitHub. 31 | Using the additional flags below, you can increase the log level to debug potential problems with the issue tracker integrations. 32 | * add additional CLI arguments (e.g. `-v`, `-debug`) 33 | * By default this plugin uses the latest released version of Nuclei. 34 | In the rare case if a new major version is not backward compatible with the CLI interface used by the plugin, you can manually choose an older version to temporarily work around the issue. 35 | Please create a ticket to request updating the plugin. 36 | 37 | ![Nuclei plugin](/static/nuclei-plugin.png) 38 | 39 | ## Building it manually 40 | 1. You can build the code using Maven within the root directory where the `pom.xml` resides. 41 | * `mvn clean package -DskipTests` 42 | 2. The built artifact can be found under `./target/nuclei.hpi` 43 | 3. You can start a Jenkins deployment with the plugin pre-installed using: `mvn hpi:run` 44 | * To enable debugging use `mvnDebug hpi:run`, then attach a remote debugger by adding the following parameters to your run configuration: `-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000` 45 | 46 | Before creating a pull request, please make sure to do the following steps: 47 | 1. Run the tests withing the project (remove the `-DskipTests` flag) 48 | 2. Test the plugin in a fully-fledged Jenkins instance: 49 | * You can "install" the plugin by copying/overwriting the `nuclei.hpi` file within the `/plugins` (e.g. `/var/lib/jenkins/plugins/nuclei.hpi`) 50 | * Make sure to restart the Jenkins service (`sudo service jenkins restart`) 51 | 3. Test the plugin execution on the Primary (master) node and remote agents as well 52 | 53 | ### Starting fresh 54 | 1. Delete the compiled classes and generated artifacts within the `target` folder: `mvn clean` 55 | 2. Remove the Nuclei configuration from the current user (if any): `rm -rf ~/.config/nuclei` 56 | 3. Remove the Nuclei configuration from the _jenkins_ user (if any): `sudo rm -rf /.config/nuclei` (e.g. `/var/lib/jenkins/.config/nuclei`) 57 | 4. Remove the Nuclei binary, its templates and the generated output: `sudo rm -rf /workspace//nuclei*` 58 | 5. Connect to the remote agent and do the same 59 | 60 | ## Limitations 61 | * Freestyle project support only (no pipelines) 62 | * No bundled scanner binary, the agents require internet access 63 | 64 | ## Nuclei documentation 65 | * [https://nuclei.projectdiscovery.io](https://nuclei.projectdiscovery.io) 66 | * [https://github.com/projectdiscovery/nuclei](https://github.com/projectdiscovery/nuclei) -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | org.jenkins-ci.plugins 6 | plugin 7 | 4.34 8 | 9 | 10 | 11 | io.jenkins.plugins 12 | nuclei 13 | ${revision}${changelist} 14 | hpi 15 | 16 | Nuclei Plugin 17 | Open-source vulnerability scanning plugin using Project Discovery's Nuclei tool 18 | ${pluginUrl} 19 | 20 | 21 | Project Discovery 22 | https://projectdiscovery.io 23 | 24 | 25 | 26 | 27 | MIT License 28 | https://opensource.org/licenses/MIT 29 | 30 | 31 | 32 | 33 | 34 | forgedhallpass 35 | istvan@projectdiscovery.io 36 | Project Discovery 37 | https://projectdiscovery.io 38 | 39 | 40 | 41 | 42 | scm:git:https://github.com/jenkinsci/${pluginName}.git 43 | scm:git:git@github.com:jenkinsci/${pluginName}.git 44 | ${pluginUrl} 45 | HEAD 46 | 47 | 48 | 49 | GitHub Issues 50 | ${pluginUrl}/issues 51 | 52 | 53 | 54 | 1.0.1 55 | -SNAPSHOT 56 | 57 | ${project.artifactId}-plugin 58 | https://github.com/jenkinsci/${pluginName} 59 | 60 | 61 | 2.289.1 62 | 8 63 | 64 | 1.19 65 | 1.15.3 66 | 67 | 68 | 69 | 70 | org.jenkins-ci.plugins 71 | structs 72 | ${structs.version} 73 | 74 | 75 | 76 | org.jsoup 77 | jsoup 78 | ${jsoup.version} 79 | 80 | 81 | 82 | junit 83 | junit 84 | test 85 | 86 | 87 | 88 | 89 | 90 | repo.jenkins-ci.org 91 | https://repo.jenkins-ci.org/public/ 92 | 93 | 94 | 95 | 96 | repo.jenkins-ci.org 97 | https://repo.jenkins-ci.org/public/ 98 | 99 | 100 | 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-javadoc-plugin 106 | 107 | 108 | attach-javadocs 109 | 110 | jar 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /src/main/java/io/projectdiscovery/plugins/jenkins/nuclei/CompressionUtil.java: -------------------------------------------------------------------------------- 1 | package io.projectdiscovery.plugins.jenkins.nuclei; 2 | 3 | import hudson.FilePath; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.net.URL; 8 | 9 | /** 10 | * Utility class for unpacking data obtained from the given {@link URL} or {@link InputStream} 11 | */ 12 | public final class CompressionUtil { 13 | 14 | private CompressionUtil() {} 15 | 16 | public static void unTarGz(URL url, FilePath pathOutput) throws IOException { 17 | try (final InputStream inputStream = url.openStream()) { 18 | unTarGz(inputStream, pathOutput); 19 | } 20 | } 21 | 22 | public static void unTarGz(InputStream inputStream, FilePath pathOutput) throws IOException { 23 | try { 24 | pathOutput.untarFrom(inputStream, FilePath.TarCompression.GZIP); 25 | } catch (InterruptedException e) { 26 | throw new IllegalStateException("Thread interrupted while trying to uncompress the file!"); 27 | } 28 | } 29 | 30 | public static void unZip(URL url, FilePath pathOutput) throws IOException { 31 | try (final InputStream inputStream = url.openStream()) { 32 | unZip(inputStream, pathOutput); 33 | } 34 | } 35 | 36 | public static void unZip(InputStream inputStream, FilePath pathOutput) throws IOException { 37 | try { 38 | pathOutput.unzipFrom(inputStream); 39 | } catch (InterruptedException e) { 40 | throw new IllegalStateException("Thread interrupted while trying to uncompress the file!"); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/io/projectdiscovery/plugins/jenkins/nuclei/NucleiBuilder.java: -------------------------------------------------------------------------------- 1 | package io.projectdiscovery.plugins.jenkins.nuclei; 2 | 3 | import edu.umd.cs.findbugs.annotations.NonNull; 4 | 5 | import hudson.Extension; 6 | import hudson.FilePath; 7 | import hudson.Launcher; 8 | import hudson.model.AbstractProject; 9 | import hudson.model.Run; 10 | import hudson.model.TaskListener; 11 | import hudson.tasks.BuildStep; 12 | import hudson.tasks.BuildStepDescriptor; 13 | import hudson.tasks.Builder; 14 | import hudson.util.FormValidation; 15 | import hudson.util.ListBoxModel; 16 | import jenkins.tasks.SimpleBuildStep; 17 | import org.kohsuke.stapler.DataBoundConstructor; 18 | import org.kohsuke.stapler.DataBoundSetter; 19 | import org.kohsuke.stapler.QueryParameter; 20 | import org.kohsuke.stapler.verb.POST; 21 | 22 | import java.io.IOException; 23 | import java.io.PrintStream; 24 | import java.nio.charset.StandardCharsets; 25 | import java.util.ArrayList; 26 | import java.util.Arrays; 27 | import java.util.List; 28 | 29 | /** 30 | * The {@link BuildStep} that performs the actual Build. 31 | */ 32 | public class NucleiBuilder extends Builder implements SimpleBuildStep { 33 | 34 | /** 35 | * The fields must either be public or have public getters in order for Jenkins to be able to re-populate them on job configuration re-load. 36 | * The name of the fields must match the ones specified in config.jelly 37 | */ 38 | private final String targetUrl; 39 | private String additionalFlags; 40 | private String reportingConfiguration; 41 | private String nucleiVersion; 42 | 43 | @DataBoundConstructor 44 | public NucleiBuilder(String targetUrl) { 45 | this.targetUrl = targetUrl; 46 | } 47 | 48 | @SuppressWarnings("unused") 49 | @DataBoundSetter 50 | public void setAdditionalFlags(String additionalFlags) { 51 | this.additionalFlags = additionalFlags; 52 | } 53 | 54 | @SuppressWarnings("unused") 55 | @DataBoundSetter 56 | public void setReportingConfiguration(String reportingConfiguration) { 57 | this.reportingConfiguration = reportingConfiguration; 58 | } 59 | 60 | @SuppressWarnings("unused") 61 | @DataBoundSetter 62 | public void setNucleiVersion(String nucleiVersion) { 63 | this.nucleiVersion = nucleiVersion; 64 | } 65 | 66 | /** 67 | * Getter is used by Jenkins to set the previously configured values within a job configuration. 68 | * Re-opening the configuration of an existing job should reload the previous values. 69 | */ 70 | @SuppressWarnings("unused") 71 | public String getTargetUrl() { 72 | return targetUrl; 73 | } 74 | 75 | @SuppressWarnings("unused") 76 | public String getReportingConfiguration() { 77 | return reportingConfiguration; 78 | } 79 | 80 | @SuppressWarnings("unused") 81 | public String getAdditionalFlags() { 82 | return additionalFlags; 83 | } 84 | 85 | @SuppressWarnings("unused") 86 | public String getNucleiVersion() { 87 | return nucleiVersion; 88 | } 89 | 90 | @Override 91 | public void perform(@NonNull Run run, @NonNull FilePath workspace, @NonNull Launcher launcher, TaskListener listener) { 92 | final PrintStream logger = listener.getLogger(); 93 | 94 | final FilePath workingDirectory = NucleiBuilderHelper.getWorkingDirectory(launcher, workspace, logger); 95 | final String nucleiBinaryPath = NucleiBuilderHelper.prepareNucleiBinary(workingDirectory, this.nucleiVersion, logger); 96 | final String[] resultCommand = createScanCommand(run, launcher, logger, workingDirectory, nucleiBinaryPath); 97 | 98 | NucleiBuilderHelper.runCommand(logger, launcher, resultCommand); 99 | } 100 | 101 | private String[] createScanCommand(Run run, Launcher launcher, PrintStream logger, FilePath workingDirectory, String nucleiBinaryPath) { 102 | final List cliArguments = createMandatoryCliArguments(run, launcher, logger, workingDirectory, nucleiBinaryPath); 103 | addIssueTrackerConfig(workingDirectory, cliArguments); 104 | return NucleiBuilderHelper.mergeCliArguments(cliArguments, this.additionalFlags); 105 | } 106 | 107 | private List createMandatoryCliArguments(Run run, Launcher launcher, PrintStream logger, FilePath filePathWorkingDirectory, String nucleiPath) { 108 | final String nucleiTemplatesPath = NucleiBuilderHelper.downloadTemplates(launcher, filePathWorkingDirectory, nucleiPath, logger); 109 | final FilePath outputFilePath = NucleiBuilderHelper.resolveFilePath(filePathWorkingDirectory, String.format("nucleiOutput-%s.txt", run.getId())); 110 | return new ArrayList<>(Arrays.asList(nucleiPath, 111 | "-templates", nucleiTemplatesPath, 112 | "-target", this.targetUrl, 113 | "-output", outputFilePath.getRemote(), 114 | "-no-color")); 115 | } 116 | 117 | private void addIssueTrackerConfig(FilePath workingDirectory, List cliArguments) { 118 | if (this.reportingConfiguration != null && !this.reportingConfiguration.isEmpty()) { 119 | final FilePath reportConfigPath = NucleiBuilderHelper.resolveFilePath(workingDirectory, "reporting_config.yml"); 120 | try { 121 | reportConfigPath.write(this.reportingConfiguration, StandardCharsets.UTF_8.name()); 122 | 123 | cliArguments.add("-report-config"); 124 | cliArguments.add(reportConfigPath.getRemote()); 125 | } catch (IOException | InterruptedException e) { 126 | throw new IllegalStateException(String.format("Error while writing the reporting/issue tracking configuration to '%s'", reportConfigPath.getRemote())); 127 | } 128 | } 129 | } 130 | 131 | @SuppressWarnings("unused") 132 | @Extension 133 | public static final class DescriptorImpl extends BuildStepDescriptor { 134 | 135 | /** 136 | * This method is called by Jenkins to validate the input fields before saving the job's configuration. 137 | * The name of the method must start with doCheck followed by the name of one of the fields declared in the config.jelly, 138 | * using standard Java naming conventions and must return {@link FormValidation}. 139 | * The fields intended for validation must match the name of the fields within config.jelly and has to be annotated with {@link QueryParameter}. 140 | * 141 | * @param targetUrl The URL of the desired application to be tested (mandatory) 142 | * @param additionalFlags Additional CLI arguments (e.g. -v -debug) 143 | * @param reportingConfiguration Issue tracker configuration (e.g. Jira/GitHub) 144 | * @return {@link FormValidation#ok()} or {@link FormValidation#error(java.lang.String)} in case of a validation error. 145 | */ 146 | @SuppressWarnings("unused") 147 | @POST 148 | public FormValidation doCheckTargetUrl(@QueryParameter String targetUrl, @QueryParameter String additionalFlags, @QueryParameter String reportingConfiguration, @QueryParameter String nucleiVersion) { 149 | if (targetUrl.isEmpty()) { 150 | return FormValidation.error(Messages.NucleiBuilder_DescriptorImpl_errors_missingUrl()); 151 | } 152 | 153 | if (nucleiVersion.isEmpty()) { 154 | return FormValidation.error(Messages.NucleiBuilder_DescriptorImpl_errors_missingVersion()); 155 | } else if (!NucleiDownloadHelper.NUCLEI_VERSION_PATTERN.matcher(nucleiVersion).matches()) { 156 | return FormValidation.error(Messages.NucleiBuilder_DescriptorImpl_errors_incorrectVersion()); 157 | } 158 | 159 | return FormValidation.ok(); 160 | } 161 | 162 | /** 163 | * Method called by Jenkins to populate the "Nuclei version" drop-down. 164 | * 165 | * @return the Nuclei versions retrieved from the GitHub release page in a vX.Y.Z format. 166 | */ 167 | @SuppressWarnings("unused") 168 | public ListBoxModel doFillNucleiVersionItems() { 169 | return NucleiDownloadHelper.getNucleiVersions().stream() 170 | .map(ListBoxModel.Option::new) 171 | .collect(ListBoxModel::new, ArrayList::add, ArrayList::addAll); 172 | } 173 | 174 | @Override 175 | public boolean isApplicable(Class aClass) { 176 | return true; 177 | } 178 | 179 | @NonNull 180 | @Override 181 | public String getDisplayName() { 182 | return Messages.NucleiBuilder_DescriptorImpl_DisplayName(); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/main/java/io/projectdiscovery/plugins/jenkins/nuclei/NucleiBuilderHelper.java: -------------------------------------------------------------------------------- 1 | package io.projectdiscovery.plugins.jenkins.nuclei; 2 | 3 | import hudson.FilePath; 4 | import hudson.Launcher; 5 | import hudson.Proc; 6 | import hudson.remoting.VirtualChannel; 7 | 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.io.PrintStream; 11 | import java.net.URL; 12 | import java.util.Arrays; 13 | import java.util.List; 14 | import java.util.function.Predicate; 15 | import java.util.stream.Stream; 16 | 17 | public final class NucleiBuilderHelper { 18 | 19 | private static final String NUCLEI_BINARY_NAME = "nuclei"; 20 | 21 | private NucleiBuilderHelper() {} 22 | 23 | static String[] mergeCliArguments(List mandatoryCommands, String additionalFlags) { 24 | return mergeCliArguments(mandatoryCommands.toArray(new String[0]), additionalFlags); 25 | } 26 | 27 | static String[] mergeCliArguments(String[] mandatoryCommands, String additionalFlags) { 28 | final String[] result; 29 | 30 | if (additionalFlags == null || additionalFlags.isEmpty()) { 31 | result = mandatoryCommands; 32 | } else { 33 | final Stream additionalFlagStream = Arrays.stream(additionalFlags.split("(?= -)")) 34 | .map(String::trim) 35 | .flatMap(v -> Arrays.stream(v.split(" ", 2)).map(String::trim)) 36 | .map(v -> { 37 | final Predicate tester = (character) -> v.startsWith(character) && v.endsWith(character); 38 | return tester.test("\"") || tester.test("'") ? v.substring(1, v.length() - 1) : v; 39 | }); 40 | result = Stream.concat(Arrays.stream(mandatoryCommands), additionalFlagStream).toArray(String[]::new); 41 | } 42 | 43 | return result; 44 | } 45 | 46 | static void runCommand(PrintStream logger, Launcher launcher, String[] command) { 47 | try { 48 | Launcher.ProcStarter procStarter = launcher.launch(); 49 | 50 | Proc process = procStarter.cmds(command).stdout(logger).stderr(logger).start(); 51 | 52 | process.join(); 53 | } catch (IOException | InterruptedException e) { 54 | logger.println("Error while trying to run the following command: " + String.join(" ", command)); 55 | } 56 | } 57 | 58 | static FilePath resolveFilePath(FilePath directory, String fileName) { 59 | final String absolutePath = directory.getRemote(); 60 | 61 | final String resultPath = absolutePath.endsWith(File.separator) ? (absolutePath + fileName) 62 | : (absolutePath + File.separator + fileName); 63 | return new FilePath(directory.getChannel(), resultPath); 64 | } 65 | 66 | static String downloadTemplates(Launcher launcher, FilePath workingDirectory, String nucleiPath, PrintStream logger) { 67 | final String nucleiTemplatesPath = resolveFilePath(workingDirectory, "nuclei-templates").getRemote(); 68 | runCommand(logger, launcher, new String[]{nucleiPath, 69 | "-update-directory", nucleiTemplatesPath, 70 | "-update-templates", 71 | "-no-color"}); 72 | return nucleiTemplatesPath; 73 | } 74 | 75 | static String prepareNucleiBinary(FilePath workingDirectory, String nucleiVersion, PrintStream logger) { 76 | try { 77 | final SupportedOperatingSystem operatingSystem = SupportedOperatingSystem.getType(System.getProperty("os.name")); 78 | logger.println("Retrieved operating system: " + operatingSystem); 79 | 80 | final String nucleiBinaryName = operatingSystem == SupportedOperatingSystem.Windows ? NUCLEI_BINARY_NAME + ".exe" 81 | : NUCLEI_BINARY_NAME; 82 | final FilePath nucleiPath = NucleiBuilderHelper.resolveFilePath(workingDirectory, nucleiBinaryName); 83 | 84 | if (nucleiPath.exists()) { 85 | final int fullPermissionToOwner = 0700; 86 | nucleiPath.chmod(fullPermissionToOwner); 87 | } else { 88 | final SupportedArchitecture architecture = SupportedArchitecture.getType(System.getProperty("os.arch")); 89 | downloadAndUnpackNuclei(operatingSystem, architecture, workingDirectory, nucleiVersion); 90 | } 91 | 92 | return nucleiPath.getRemote(); 93 | } catch (IOException | InterruptedException e) { 94 | throw new IllegalStateException("Error while obtaining Nuclei binary."); 95 | } 96 | } 97 | 98 | static FilePath getWorkingDirectory(Launcher launcher, FilePath workspace, PrintStream logger) { 99 | final VirtualChannel virtualChannel = launcher.getChannel(); 100 | if (virtualChannel == null) { 101 | throw new IllegalStateException("The agent does not support remote operations!"); 102 | } 103 | 104 | final FilePath workingDirectory = new FilePath(virtualChannel, workspace.getRemote()); 105 | logger.println("Working directory: " + workingDirectory); 106 | return workingDirectory; 107 | } 108 | 109 | private static void downloadAndUnpackNuclei(SupportedOperatingSystem operatingSystem, SupportedArchitecture architecture, FilePath workingDirectory, String nucleiVersion) throws IOException { 110 | final URL downloadUrl = NucleiDownloadHelper.createDownloadUrl(operatingSystem, architecture, nucleiVersion); 111 | 112 | final String downloadFilePath = downloadUrl.getPath().toLowerCase(); 113 | if (downloadFilePath.endsWith(".zip")) { 114 | CompressionUtil.unZip(downloadUrl, workingDirectory); 115 | } else if (downloadFilePath.endsWith(".tar.gz")) { 116 | CompressionUtil.unTarGz(downloadUrl, workingDirectory); 117 | } else { 118 | throw new IllegalStateException(String.format("Unsupported file type ('%s'). It should be '.tar.gz' or '.zip'!", downloadFilePath)); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/io/projectdiscovery/plugins/jenkins/nuclei/NucleiDownloadHelper.java: -------------------------------------------------------------------------------- 1 | package io.projectdiscovery.plugins.jenkins.nuclei; 2 | 3 | import org.jsoup.Jsoup; 4 | import org.jsoup.nodes.Element; 5 | 6 | import java.io.IOException; 7 | import java.net.MalformedURLException; 8 | import java.net.URL; 9 | import java.util.List; 10 | import java.util.Objects; 11 | import java.util.function.Supplier; 12 | import java.util.regex.Matcher; 13 | import java.util.regex.Pattern; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * Utility class that helps extracting information about Nuclei releases from GitHub. 18 | */ 19 | public final class NucleiDownloadHelper { 20 | 21 | private static final String NUCLEI_VERSION_REGEX = "((?:\\d+\\.)+\\d+|\\d+)"; 22 | public static final Pattern NUCLEI_VERSION_PATTERN = Pattern.compile(NUCLEI_VERSION_REGEX); 23 | 24 | private static final String NUCLEI_RELEASE_URL = "https://github.com/projectdiscovery/nuclei/releases"; 25 | private static final Pattern RELEASE_TAG_URL_PATTERN = Pattern.compile("/projectdiscovery/nuclei/releases/tag/v" + NUCLEI_VERSION_REGEX); 26 | 27 | private NucleiDownloadHelper() {} 28 | 29 | /** 30 | * @return a list of released versions of Nuclei to GitHub 31 | */ 32 | public static List getNucleiVersions() { 33 | return getNucleiVersions(getNucleiReleasePageBody()); 34 | } 35 | 36 | /** 37 | * @param operatingSystem The identified operating system mapped to a supported Nuclei build. 38 | * @param architecture The identified architecture mapped to a supported Nuclei build. 39 | * @param version An existing version of Nuclei, retrieved by {@link NucleiDownloadHelper#getNucleiVersions()} 40 | * @return The URL from which the desired Nuclei build can be downloaded. 41 | */ 42 | public static URL createDownloadUrl(SupportedOperatingSystem operatingSystem, SupportedArchitecture architecture, String version) { 43 | final Element nucleiReleasePageBody = getNucleiReleasePageBody(); 44 | return createDownloadUrl(nucleiReleasePageBody, operatingSystem, architecture, version); 45 | } 46 | 47 | private static Element getNucleiReleasePageBody() { 48 | final Element documentBody; 49 | try { 50 | documentBody = Jsoup.connect(NUCLEI_RELEASE_URL).get().body(); 51 | } catch (IOException e) { 52 | throw new IllegalStateException(String.format("Could not access the Nuclei GitHub release URL (%s)", NUCLEI_RELEASE_URL)); 53 | } 54 | return documentBody; 55 | } 56 | 57 | private static List getNucleiVersions(Element documentBody) { 58 | 59 | return documentBody.select("div.release-header a") 60 | .stream() 61 | .map(e -> e.attr("href")) 62 | .map(url -> { 63 | final Matcher matcher = RELEASE_TAG_URL_PATTERN.matcher(url); 64 | return matcher.find() ? matcher.group(1) : null; 65 | }) 66 | .filter(Objects::nonNull) 67 | .collect(Collectors.toList()); 68 | } 69 | 70 | private static URL createDownloadUrl(Element documentBody, SupportedOperatingSystem operatingSystem, SupportedArchitecture architecture, String version) { 71 | final Element downloadUrlElement = documentBody.selectFirst(String.format("a[href~=nuclei_%s_%s_%s.]", version, operatingSystem.getDisplayName(), architecture.getDisplayName())); 72 | 73 | final Supplier urlNotFoundException = () -> new IllegalStateException(String.format("Could not identify the download URL for version (%s), platform (%s), architecture(%s)", version, operatingSystem, architecture)); 74 | 75 | if (downloadUrlElement == null) { 76 | throw urlNotFoundException.get(); 77 | } 78 | 79 | final String downloadUrl = downloadUrlElement.attr("abs:href"); 80 | if (downloadUrl.isEmpty()) { 81 | throw urlNotFoundException.get(); 82 | } 83 | 84 | try { 85 | return new URL(downloadUrl); 86 | } catch (MalformedURLException e) { 87 | throw new IllegalStateException(String.format("The extracted download URL '%s' is not valid!", downloadUrl)); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/io/projectdiscovery/plugins/jenkins/nuclei/SupportedArchitecture.java: -------------------------------------------------------------------------------- 1 | package io.projectdiscovery.plugins.jenkins.nuclei; 2 | 3 | import java.util.stream.Stream; 4 | 5 | /** 6 | * Class that maps the OS architecture to supported Nuclei builds.
7 | * 8 | * Possible values: https://github.com/golang/go/blob/master/src/go/build/syslist.go 9 | */ 10 | public enum SupportedArchitecture { 11 | i386("386"), AMD64("amd64"), ARM64("arm64"), ARM("armv6"); 12 | 13 | private final String value; 14 | SupportedArchitecture(final String value) { 15 | this.value = value; 16 | } 17 | 18 | public String getDisplayName() { 19 | return value; 20 | } 21 | 22 | public static SupportedArchitecture getType(final String value) { 23 | final SupportedArchitecture result; 24 | 25 | if (value != null && !value.isEmpty()) { 26 | if (Stream.of("mips", "ppc", "risc", "sparc", "wasm", "s390").anyMatch(value::contains)) { 27 | throw new IllegalArgumentException(String.format("Current architecture '%s' is not mapped correctly or is not supported!", value)); 28 | } else if (value.contains("64")) { 29 | result = value.contains("arm") || value.contains("aarch") ? ARM64 : AMD64; 30 | } else if (value.contains("arm")) { 31 | result = ARM; 32 | } else { 33 | result = i386; 34 | } 35 | } else { 36 | throw new IllegalArgumentException("The architecture should not be null or empty!"); 37 | } 38 | return result; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/io/projectdiscovery/plugins/jenkins/nuclei/SupportedOperatingSystem.java: -------------------------------------------------------------------------------- 1 | package io.projectdiscovery.plugins.jenkins.nuclei; 2 | 3 | /** 4 | * Class that maps the OS to supported Nuclei builds.
5 | * 6 | * Possible values: https://github.com/golang/go/blob/master/src/go/build/syslist.go 7 | */ 8 | public enum SupportedOperatingSystem { 9 | Windows("windows"), MacOS("macOS"), Linux("linux"); 10 | 11 | private final String value; 12 | SupportedOperatingSystem(final String value) { 13 | this.value = value; 14 | } 15 | 16 | public String getDisplayName() { 17 | return value; 18 | } 19 | 20 | public static SupportedOperatingSystem getType(String value) { 21 | final SupportedOperatingSystem result; 22 | if (value != null && !value.isEmpty()) { 23 | value = value.toLowerCase(); 24 | 25 | if (value.contains("win")) { 26 | result = Windows; 27 | } else if (value.contains("nux")) { 28 | result = Linux; 29 | } else if (value.contains("mac") || value.contains("darwin")) { 30 | result = MacOS; 31 | } else { 32 | throw new IllegalArgumentException(String.format("The operating system '%s' is not supported or it's not mapped correctly!", value)); 33 | } 34 | } else { 35 | throw new IllegalArgumentException("The operating system name should not be null or empty!"); 36 | } 37 | return result; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | Nuclei Open-Source Vulnerability Scanner integration
4 | For more info visit: 5 | 9 |
10 | -------------------------------------------------------------------------------- /src/main/resources/io/projectdiscovery/plugins/jenkins/nuclei/Messages.properties: -------------------------------------------------------------------------------- 1 | NucleiBuilder.DescriptorImpl.DisplayName=Nuclei Vulnerability Scanner 2 | NucleiBuilder.DescriptorImpl.errors.missingUrl=Please set the URL of the application you want to be scanned. 3 | NucleiBuilder.DescriptorImpl.errors.missingVersion=Please wait while Nuclei versions are being populated. This requires an internet connection. 4 | NucleiBuilder.DescriptorImpl.errors.incorrectVersion=Incorrect Nuclei version provided. -------------------------------------------------------------------------------- /src/main/resources/io/projectdiscovery/plugins/jenkins/nuclei/NucleiBuilder/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/resources/io/projectdiscovery/plugins/jenkins/nuclei/NucleiBuilder/config.properties: -------------------------------------------------------------------------------- 1 | TargetUrl=Target URL 2 | 3 | ReportingConfiguration=Reporting Configuration 4 | ReportingConfigurationDescription=Reporting Configuration in YAML format 5 | 6 | NucleiVersion=Nuclei version 7 | NucleiVersionDescription=Select the desired version of Nuclei to use for the vulnerability scanning. 8 | 9 | AdditionalFlags=Additional Flags 10 | AdditionalFlagsDescription=Set additional command line Options -------------------------------------------------------------------------------- /src/main/resources/io/projectdiscovery/plugins/jenkins/nuclei/NucleiBuilder/help-additionalFlags.html: -------------------------------------------------------------------------------- 1 |
2 | For more info please see

nuclei --help

or visit: 3 | 7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/io/projectdiscovery/plugins/jenkins/nuclei/NucleiBuilder/help-nucleiVersion.html: -------------------------------------------------------------------------------- 1 |
2 | By default we recommended using the latest version of Nuclei.
3 | This option is offered to handle potential compatibility issues.
4 | In case of such issues: 5 |
    6 |
  • please make sure you are using the latest version of the plugin.
  • 7 |
  • if the issue still persists choose an older version of Nuclei to maintain compatibility.
  • 8 |
  • notify the development team by creating an issue, so we can make the necessary changes.
  • 9 |
10 |
11 | -------------------------------------------------------------------------------- /src/main/resources/io/projectdiscovery/plugins/jenkins/nuclei/NucleiBuilder/help-reportingConfiguration.html: -------------------------------------------------------------------------------- 1 |
2 | Offers Reporting and Issue Tracking capabilities (e.g. Jira/GitHub etc)
3 | For the full list of supported platforms, usage and documentation please visit the "Getting Started" page 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/io/projectdiscovery/plugins/jenkins/nuclei/NucleiBuilder/help-targetUrl.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/test/java/io/projectdiscovery/plugins/jenkins/nuclei/NucleiBuilderTest.java: -------------------------------------------------------------------------------- 1 | package io.projectdiscovery.plugins.jenkins.nuclei; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | public class NucleiBuilderTest { 7 | 8 | @Test 9 | public void testCliArgumentMerger() { 10 | final String[] mandatoryArguments = {"-target", "http://localhost:8080/vulnerableApp", "-no-color"}; 11 | final String additionalFlags = "-a space-prefix -b-b space between -c-c-c two more spaces -dd \"c:/program files/asd\" -ee ''"; 12 | 13 | final String[] result = NucleiBuilderHelper.mergeCliArguments(mandatoryArguments, additionalFlags); 14 | Assert.assertArrayEquals(result, new String[]{"-target", "http://localhost:8080/vulnerableApp", 15 | "-no-color", 16 | "-a", "space-prefix", 17 | "-b-b", "space between", 18 | "-c-c-c", "two more spaces", 19 | "-dd", "c:/program files/asd", 20 | "-ee", ""}); 21 | } 22 | } -------------------------------------------------------------------------------- /static/nuclei-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/nuclei-plugin/12e088c19a51cfbcd1ca2aa6b2d3d32e6d775531/static/nuclei-logo.png -------------------------------------------------------------------------------- /static/nuclei-plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkinsci/nuclei-plugin/12e088c19a51cfbcd1ca2aa6b2d3d32e6d775531/static/nuclei-plugin.png --------------------------------------------------------------------------------