2020-06-24 22:23:37 +00:00
|
|
|
package runner
|
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/zip"
|
|
|
|
"bytes"
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
2020-06-25 15:56:44 +00:00
|
|
|
"strings"
|
2020-07-07 12:54:00 +00:00
|
|
|
"time"
|
2020-06-24 22:23:37 +00:00
|
|
|
|
|
|
|
"github.com/blang/semver"
|
|
|
|
"github.com/google/go-github/v32/github"
|
|
|
|
jsoniter "github.com/json-iterator/go"
|
|
|
|
"github.com/projectdiscovery/gologger"
|
|
|
|
)
|
|
|
|
|
|
|
|
// nucleiConfig contains some configuration options for nuclei
|
|
|
|
type nucleiConfig struct {
|
2020-07-07 12:54:00 +00:00
|
|
|
TemplatesDirectory string `json:"templates-directory,omitempty"`
|
|
|
|
CurrentVersion string `json:"current-version,omitempty"`
|
|
|
|
LastChecked time.Time `json:"last-checked,omitempty"`
|
2020-06-24 22:23:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// nucleiConfigFilename is the filename of nuclei configuration file.
|
|
|
|
const nucleiConfigFilename = ".nuclei-config.json"
|
|
|
|
|
|
|
|
var reVersion = regexp.MustCompile(`\d+\.\d+\.\d+`)
|
|
|
|
|
|
|
|
// readConfiguration reads the nuclei configuration file from disk.
|
|
|
|
func (r *Runner) readConfiguration() (*nucleiConfig, error) {
|
|
|
|
home, err := os.UserHomeDir()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
templatesConfigFile := path.Join(home, nucleiConfigFilename)
|
|
|
|
file, err := os.Open(templatesConfigFile)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
config := &nucleiConfig{}
|
|
|
|
if err = jsoniter.NewDecoder(file).Decode(config); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return config, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// readConfiguration reads the nuclei configuration file from disk.
|
|
|
|
func (r *Runner) writeConfiguration(config *nucleiConfig) error {
|
|
|
|
home, err := os.UserHomeDir()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-07-08 11:55:29 +00:00
|
|
|
config.LastChecked = time.Now()
|
2020-06-24 22:23:37 +00:00
|
|
|
templatesConfigFile := path.Join(home, nucleiConfigFilename)
|
2020-06-25 15:56:44 +00:00
|
|
|
file, err := os.OpenFile(templatesConfigFile, os.O_WRONLY|os.O_CREATE, 0777)
|
2020-06-24 22:23:37 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
2020-06-25 15:56:44 +00:00
|
|
|
if err = jsoniter.NewEncoder(file).Encode(config); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
2020-06-24 22:23:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// updateTemplates checks if the default list of nuclei-templates
|
|
|
|
// exist in the users home directory, if not the latest revision
|
|
|
|
// is downloaded from github.
|
|
|
|
//
|
|
|
|
// If the path exists but is not latest, the new version is downloaded
|
|
|
|
// from github and replaced with the templates directory.
|
|
|
|
func (r *Runner) updateTemplates() error {
|
|
|
|
home, err := os.UserHomeDir()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
templatesConfigFile := path.Join(home, nucleiConfigFilename)
|
|
|
|
if _, err := os.Stat(templatesConfigFile); !os.IsNotExist(err) {
|
|
|
|
config, err := r.readConfiguration()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
r.templatesConfig = config
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.templatesConfig == nil || (r.options.TemplatesDirectory != "" && r.templatesConfig.TemplatesDirectory != r.options.TemplatesDirectory) {
|
|
|
|
if !r.options.UpdateTemplates {
|
2020-06-26 12:14:15 +00:00
|
|
|
gologger.Labelf("nuclei-templates are not installed, use update-templates flag.\n")
|
2020-06-25 15:56:44 +00:00
|
|
|
return nil
|
2020-06-24 22:23:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Use custom location if user has given a template directory
|
|
|
|
if r.options.TemplatesDirectory != "" {
|
|
|
|
home = r.options.TemplatesDirectory
|
|
|
|
}
|
2020-07-08 11:55:29 +00:00
|
|
|
r.templatesConfig = &nucleiConfig{TemplatesDirectory: path.Join(home, "nuclei-templates")}
|
2020-06-25 15:56:44 +00:00
|
|
|
os.RemoveAll(r.templatesConfig.TemplatesDirectory)
|
2020-06-24 22:23:37 +00:00
|
|
|
|
|
|
|
// Download the repository and also write the revision to a HEAD file.
|
|
|
|
version, asset, err := r.getLatestReleaseFromGithub()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
gologger.Verbosef("Downloading nuclei-templates (v%s) to %s\n", "update-templates", version.String(), r.templatesConfig.TemplatesDirectory)
|
|
|
|
|
|
|
|
if err = r.downloadReleaseAndUnzip(asset.GetZipballURL()); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
r.templatesConfig.CurrentVersion = version.String()
|
|
|
|
if err = r.writeConfiguration(r.templatesConfig); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-06-25 15:56:44 +00:00
|
|
|
gologger.Infof("Successfully downloaded nuclei-templates (v%s). Enjoy!\n", version.String())
|
2020-06-24 22:23:37 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-07 12:54:00 +00:00
|
|
|
// Check if last checked is more than 24 hours.
|
|
|
|
// If not, return since we don't want to do anything now.
|
2020-07-07 13:03:35 +00:00
|
|
|
if time.Now().Sub(r.templatesConfig.LastChecked) < 24*time.Hour && !r.options.UpdateTemplates {
|
2020-07-07 12:54:00 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-06-24 22:23:37 +00:00
|
|
|
// Get the configuration currently on disk.
|
|
|
|
verText := r.templatesConfig.CurrentVersion
|
|
|
|
indices := reVersion.FindStringIndex(verText)
|
|
|
|
if indices == nil {
|
|
|
|
return fmt.Errorf("invalid release found with tag %s", err)
|
|
|
|
}
|
|
|
|
if indices[0] > 0 {
|
|
|
|
verText = verText[indices[0]:]
|
|
|
|
}
|
|
|
|
oldVersion, err := semver.Make(verText)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
version, asset, err := r.getLatestReleaseFromGithub()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if version.EQ(oldVersion) {
|
2020-06-26 12:14:15 +00:00
|
|
|
gologger.Labelf("Latest version of nuclei-templates installed: v%s\n", oldVersion.String())
|
2020-07-07 12:54:00 +00:00
|
|
|
return r.writeConfiguration(r.templatesConfig)
|
2020-06-24 22:23:37 +00:00
|
|
|
}
|
|
|
|
if version.GT(oldVersion) {
|
|
|
|
if !r.options.UpdateTemplates {
|
2020-06-26 12:14:15 +00:00
|
|
|
gologger.Labelf("You're using outdated nuclei-templates. Latest v%s\n", version.String())
|
2020-07-07 12:54:00 +00:00
|
|
|
return r.writeConfiguration(r.templatesConfig)
|
2020-06-24 22:23:37 +00:00
|
|
|
}
|
|
|
|
if r.options.TemplatesDirectory != "" {
|
|
|
|
home = r.options.TemplatesDirectory
|
2020-06-25 15:56:44 +00:00
|
|
|
r.templatesConfig.TemplatesDirectory = path.Join(home, "nuclei-templates")
|
2020-06-24 22:23:37 +00:00
|
|
|
}
|
2020-06-25 15:56:44 +00:00
|
|
|
r.templatesConfig.CurrentVersion = version.String()
|
|
|
|
os.RemoveAll(r.templatesConfig.TemplatesDirectory)
|
|
|
|
|
2020-06-24 22:23:37 +00:00
|
|
|
gologger.Verbosef("Downloading nuclei-templates (v%s) to %s\n", "update-templates", version.String(), r.templatesConfig.TemplatesDirectory)
|
|
|
|
|
|
|
|
if err = r.downloadReleaseAndUnzip(asset.GetZipballURL()); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err = r.writeConfiguration(r.templatesConfig); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-06-25 15:56:44 +00:00
|
|
|
gologger.Infof("Successfully updated nuclei-templates (v%s). Enjoy!\n", version.String())
|
2020-06-24 22:23:37 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
const (
|
|
|
|
userName = "projectdiscovery"
|
|
|
|
repoName = "nuclei-templates"
|
|
|
|
)
|
|
|
|
|
|
|
|
// getLatestReleaseFromGithub returns the latest release from github
|
|
|
|
func (r *Runner) getLatestReleaseFromGithub() (semver.Version, *github.RepositoryRelease, error) {
|
|
|
|
client := github.NewClient(nil)
|
|
|
|
|
|
|
|
rels, _, err := client.Repositories.ListReleases(context.Background(), userName, repoName, nil)
|
|
|
|
if err != nil {
|
|
|
|
return semver.Version{}, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find the most recent version based on semantic versioning.
|
|
|
|
var latestRelease semver.Version
|
|
|
|
var latestPublish *github.RepositoryRelease
|
|
|
|
for _, release := range rels {
|
|
|
|
verText := release.GetTagName()
|
|
|
|
indices := reVersion.FindStringIndex(verText)
|
|
|
|
if indices == nil {
|
|
|
|
return semver.Version{}, nil, fmt.Errorf("invalid release found with tag %s", err)
|
|
|
|
}
|
|
|
|
if indices[0] > 0 {
|
|
|
|
verText = verText[indices[0]:]
|
|
|
|
}
|
|
|
|
ver, err := semver.Make(verText)
|
|
|
|
if err != nil {
|
|
|
|
return semver.Version{}, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if latestPublish == nil || ver.GTE(latestRelease) {
|
|
|
|
latestRelease = ver
|
|
|
|
latestPublish = release
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if latestPublish == nil {
|
|
|
|
return semver.Version{}, nil, errors.New("no version found for the templates")
|
|
|
|
}
|
|
|
|
return latestRelease, latestPublish, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// downloadReleaseAndUnzip downloads and unzips the release in a directory
|
|
|
|
func (r *Runner) downloadReleaseAndUnzip(downloadURL string) error {
|
|
|
|
req, err := http.NewRequest("GET", downloadURL, nil)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed to create HTTP request to %s: %s", downloadURL, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
res, err := http.DefaultClient.Do(req)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed to download a release file from %s: %s", downloadURL, err)
|
|
|
|
}
|
|
|
|
defer res.Body.Close()
|
|
|
|
|
|
|
|
if res.StatusCode != 200 {
|
|
|
|
return fmt.Errorf("Failed to download a release file from %s: Not successful status %d", downloadURL, res.StatusCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
buf, err := ioutil.ReadAll(res.Body)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed to create buffer for zip file: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
reader := bytes.NewReader(buf)
|
|
|
|
z, err := zip.NewReader(reader, reader.Size())
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Failed to uncompress zip file: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create the template folder if it doesn't exists
|
|
|
|
os.MkdirAll(r.templatesConfig.TemplatesDirectory, os.ModePerm)
|
|
|
|
|
|
|
|
for _, file := range z.File {
|
|
|
|
directory, name := filepath.Split(file.Name)
|
2020-06-25 15:56:44 +00:00
|
|
|
if name == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
paths := strings.Split(directory, "/")
|
|
|
|
finalPath := strings.Join(paths[1:], "/")
|
2020-06-24 22:23:37 +00:00
|
|
|
|
2020-06-25 15:56:44 +00:00
|
|
|
templateDirectory := path.Join(r.templatesConfig.TemplatesDirectory, finalPath)
|
2020-06-24 22:23:37 +00:00
|
|
|
os.MkdirAll(templateDirectory, os.ModePerm)
|
|
|
|
|
|
|
|
f, err := os.Create(path.Join(templateDirectory, name))
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Could not create uncompressed file: %s", err)
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
reader, err := file.Open()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Could not open archive to extract file: %s", err)
|
|
|
|
}
|
|
|
|
io.Copy(f, reader)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2020-06-27 15:20:43 +00:00
|
|
|
|
|
|
|
// isRelative checks if a given path is a relative path
|
|
|
|
func (r *Runner) isRelative(path string) bool {
|
2020-06-29 12:33:02 +00:00
|
|
|
if strings.HasPrefix(path, "/") || strings.Contains(path, ":\\") {
|
|
|
|
return false
|
2020-06-27 15:20:43 +00:00
|
|
|
}
|
2020-06-29 12:33:02 +00:00
|
|
|
return true
|
2020-06-27 15:20:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// resolvePath gets the absolute path to the template by either
|
|
|
|
// looking in the current directory or checking the nuclei templates directory.
|
|
|
|
//
|
|
|
|
// Current directory is given preference over the nuclei-templates directory.
|
2020-07-26 20:07:03 +00:00
|
|
|
func (r *Runner) resolvePath(templateName string) (string, error) {
|
2020-06-27 15:20:43 +00:00
|
|
|
curDirectory, err := os.Getwd()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
templatePath := path.Join(curDirectory, templateName)
|
|
|
|
if _, err := os.Stat(templatePath); !os.IsNotExist(err) {
|
2020-07-26 20:07:03 +00:00
|
|
|
gologger.Debugf("Found template in current directory: %s\n", templatePath)
|
2020-06-27 15:20:43 +00:00
|
|
|
return templatePath, nil
|
|
|
|
}
|
|
|
|
if r.templatesConfig != nil {
|
|
|
|
templatePath := path.Join(r.templatesConfig.TemplatesDirectory, templateName)
|
|
|
|
if _, err := os.Stat(templatePath); !os.IsNotExist(err) {
|
2020-07-26 20:07:03 +00:00
|
|
|
gologger.Debugf("Found template in nuclei-templates directory: %s\n", templatePath)
|
2020-06-27 15:20:43 +00:00
|
|
|
return templatePath, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return "", fmt.Errorf("no such path found: %s", templateName)
|
|
|
|
}
|
2020-07-26 18:14:05 +00:00
|
|
|
|
|
|
|
func (r *Runner) resolvePathWithBaseFolder(baseFolder, templateName string) (string, error) {
|
|
|
|
templatePath := path.Join(baseFolder, templateName)
|
|
|
|
if _, err := os.Stat(templatePath); !os.IsNotExist(err) {
|
2020-07-26 20:07:03 +00:00
|
|
|
gologger.Debugf("Found template in current directory: %s\n", templatePath)
|
2020-07-26 18:14:05 +00:00
|
|
|
return templatePath, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return "", fmt.Errorf("no such path found: %s", templateName)
|
|
|
|
}
|