├── .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 |
4 |
5 |
6 | Fast and customisable vulnerability scanner based on simple YAML based DSL.
7 |
8 |
9 |
10 |
11 |
12 |
13 | # Vulnerability scanning using Nuclei
14 |
15 |
16 |
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 | 
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 extends AbstractProject> 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 |
2 | For more info visit:
3 |
7 |
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
--------------------------------------------------------------------------------