nuclei/v2/internal/runner/runner.go

684 lines
18 KiB
Go
Raw Normal View History

package runner
import (
"bufio"
2020-06-26 08:23:54 +00:00
"context"
2020-06-29 12:13:08 +00:00
"errors"
"fmt"
"github.com/logrusorgru/aurora"
"io"
"io/ioutil"
2020-07-16 14:32:42 +00:00
"net/http/cookiejar"
"os"
2020-07-23 18:06:58 +00:00
"path/filepath"
"regexp"
"strings"
"sync"
2020-07-10 07:04:38 +00:00
tengo "github.com/d5/tengo/v2"
"github.com/d5/tengo/v2/stdlib"
"github.com/karrick/godirwalk"
"github.com/projectdiscovery/gologger"
2020-07-23 18:19:19 +00:00
"github.com/projectdiscovery/nuclei/v2/internal/progress"
2020-07-24 16:12:16 +00:00
"github.com/projectdiscovery/nuclei/v2/pkg/atomicboolean"
2020-07-16 08:57:28 +00:00
"github.com/projectdiscovery/nuclei/v2/pkg/executer"
2020-07-01 10:47:24 +00:00
"github.com/projectdiscovery/nuclei/v2/pkg/requests"
"github.com/projectdiscovery/nuclei/v2/pkg/templates"
"github.com/projectdiscovery/nuclei/v2/pkg/workflows"
)
// Runner is a client for running the enumeration process.
type Runner struct {
input string
2020-07-23 18:19:19 +00:00
inputCount int64
2020-04-04 12:51:05 +00:00
// output is the output file to write if any
output *os.File
outputMutex *sync.Mutex
2020-06-24 22:23:37 +00:00
tempFile string
templatesConfig *nucleiConfig
2020-04-04 12:51:05 +00:00
// options contains configuration options for runner
options *Options
2020-07-24 16:12:16 +00:00
limiter chan struct{}
2020-07-23 18:19:19 +00:00
// progress tracking
progress progress.IProgress
// output coloring
colorizer aurora.Aurora
decolorizer *regexp.Regexp
}
// New creates a new client for running enumeration process.
func New(options *Options) (*Runner, error) {
runner := &Runner{
2020-04-04 12:51:05 +00:00
outputMutex: &sync.Mutex{},
options: options,
}
2020-04-04 11:42:29 +00:00
2020-06-24 22:23:37 +00:00
if err := runner.updateTemplates(); err != nil {
2020-07-06 07:00:02 +00:00
gologger.Warningf("Could not update templates: %s\n", err)
2020-06-24 22:23:37 +00:00
}
if (len(options.Templates) == 0 || (options.Targets == "" && !options.Stdin && options.Target == "")) && options.UpdateTemplates {
2020-06-24 22:23:37 +00:00
os.Exit(0)
}
// output coloring
useColor := !options.NoColor
runner.colorizer = aurora.NewAurora(useColor)
if useColor {
// compile a decolorization regex to cleanup file output messages
compiled, err := regexp.Compile("\\x1B\\[[0-9;]*[a-zA-Z]")
if err != nil {
return nil, err
}
runner.decolorizer = compiled
}
// If we have stdin, write it to a new file
if options.Stdin {
tempInput, err := ioutil.TempFile("", "stdin-input-*")
if err != nil {
return nil, err
}
2020-04-26 01:30:28 +00:00
if _, err := io.Copy(tempInput, os.Stdin); err != nil {
return nil, err
}
runner.tempFile = tempInput.Name()
tempInput.Close()
}
// If we have single target, write it to a new file
if options.Target != "" {
tempInput, err := ioutil.TempFile("", "stdin-input-*")
if err != nil {
return nil, err
}
2020-07-26 19:17:42 +00:00
fmt.Fprintf(tempInput, "%s\n", options.Target)
runner.tempFile = tempInput.Name()
tempInput.Close()
}
2020-07-23 18:19:19 +00:00
// Setup input, handle a list of hosts as argument
var err error
var input *os.File
2020-07-23 18:19:19 +00:00
if options.Targets != "" {
input, err = os.Open(options.Targets)
2020-07-23 18:19:19 +00:00
} else if options.Stdin || options.Target != "" {
input, err = os.Open(runner.tempFile)
2020-07-23 18:19:19 +00:00
}
if err != nil {
gologger.Fatalf("Could not open targets file '%s': %s\n", options.Targets, err)
}
// Sanitize input and pre-compute total number of targets
var usedInput = make(map[string]bool)
dupeCount := 0
sb := strings.Builder{}
scanner := bufio.NewScanner(input)
2020-07-23 18:19:19 +00:00
runner.inputCount = 0
for scanner.Scan() {
url := scanner.Text()
// skip empty lines
if len(url) == 0 {
continue
}
// deduplication
if _, ok := usedInput[url]; !ok {
usedInput[url] = true
runner.inputCount++
sb.WriteString(url)
sb.WriteString("\n")
} else {
dupeCount++
}
}
2020-07-26 13:35:26 +00:00
input.Close()
runner.input = sb.String()
if dupeCount > 0 {
gologger.Labelf("Supplied input was automatically deduplicated (%d removed).", dupeCount)
2020-07-23 18:19:19 +00:00
}
2020-04-04 12:51:05 +00:00
// Create the output file if asked
if options.Output != "" {
output, err := os.Create(options.Output)
if err != nil {
gologger.Fatalf("Could not create output file '%s': %s\n", options.Output, err)
}
runner.output = output
}
2020-07-23 18:19:19 +00:00
// Creates the progress tracking object
runner.progress = progress.NewProgress(runner.options.NoColor, !options.Silent && options.EnableProgressBar)
2020-07-23 18:19:19 +00:00
2020-07-24 16:12:16 +00:00
runner.limiter = make(chan struct{}, options.Threads)
return runner, nil
}
// Close releases all the resources and cleans up
2020-04-04 12:51:05 +00:00
func (r *Runner) Close() {
r.output.Close()
os.Remove(r.tempFile)
2020-04-04 12:51:05 +00:00
}
func isFilePath(path string) (bool, error) {
info, err := os.Stat(path)
if err != nil {
return false, err
}
return info.Mode().IsRegular(), nil
}
2020-07-19 12:24:43 +00:00
func (r *Runner) resolvePathIfRelative(path string) (string, error) {
if r.isRelative(path) {
newPath, err := r.resolvePath(path)
if err != nil {
return "", err
}
return newPath, nil
}
return path, nil
}
func isNewPath(path string, pathMap map[string]bool) bool {
if _, already := pathMap[path]; already {
gologger.Warningf("Skipping already specified path '%s'", path)
return false
}
return true
}
2020-08-02 16:33:55 +00:00
func hasMatchingSeverity(templateSeverity string, allowedSeverities []string) bool {
for _, s := range allowedSeverities {
if strings.HasPrefix(templateSeverity, s) {
return true
}
}
return false
}
// getParsedTemplatesFor parse the specified templates and returns a slice of the parsable ones, optionally filtered
// by severity, along with a flag indicating if workflows are present.
func (r *Runner) getParsedTemplatesFor(templatePaths []string, severities string) (parsedTemplates []interface{}, workflowCount int) {
workflowCount = 0
severities = strings.ToLower(severities)
allSeverities := strings.Split(severities, ",")
filterBySeverity := len(severities) > 0
for _, match := range templatePaths {
t, err := r.parse(match)
switch t.(type) {
case *templates.Template:
template := t.(*templates.Template)
id := template.ID
// only include if severity matches or no severity filtering
sev := strings.ToLower(template.Info.Severity)
if !filterBySeverity || hasMatchingSeverity(sev, allSeverities) {
parsedTemplates = append(parsedTemplates, template)
} else {
gologger.Infof("Excluding template %s due to severity filter (%s not in [%s])", id, sev, severities)
}
case *workflows.Workflow:
workflow := t.(*workflows.Workflow)
parsedTemplates = append(parsedTemplates, workflow)
workflowCount++
default:
gologger.Errorf("Could not parse file '%s': %s\n", match, err)
}
}
return parsedTemplates, workflowCount
}
2020-04-04 19:48:57 +00:00
// RunEnumeration sets up the input layer for giving input nuclei.
// binary and runs the actual enumeration
func (r *Runner) RunEnumeration() {
// keeps track of processed dirs and files
processed := make(map[string]bool)
allTemplates := []string{}
// parses user input, handle file/directory cases and produce a list of unique templates
for _, t := range r.options.Templates {
var absPath string
var err error
if strings.Contains(t, "*") {
dirs := strings.Split(t, "/")
priorDir := strings.Join(dirs[:len(dirs)-1], "/")
absPath, err = r.resolvePathIfRelative(priorDir)
absPath += "/" + dirs[len(dirs)-1]
} else {
// resolve and convert relative to absolute path
absPath, err = r.resolvePathIfRelative(t)
}
if err != nil {
gologger.Errorf("Could not find template file '%s': %s\n", t, err)
continue
}
2020-07-23 18:06:58 +00:00
// Template input includes a wildcard
if strings.Contains(absPath, "*") {
matches := []string{}
matches, err = filepath.Glob(absPath)
if err != nil {
gologger.Labelf("Wildcard found, but unable to glob '%s': %s\n", absPath, err)
continue
}
2020-04-22 20:45:02 +00:00
2020-07-23 18:06:58 +00:00
for _, i := range matches {
processed[i] = true
}
// couldn't find templates in directory
if len(matches) == 0 {
gologger.Labelf("Error, no templates were found with '%s'.\n", absPath)
continue
2020-07-23 18:06:58 +00:00
} else {
gologger.Labelf("Identified %d templates\n", len(matches))
2020-06-26 12:37:55 +00:00
}
allTemplates = append(allTemplates, matches...)
2020-07-23 18:06:58 +00:00
} else {
// determine file/directory
isFile, err := isFilePath(absPath)
if err != nil {
gologger.Errorf("Could not stat '%s': %s\n", absPath, err)
continue
}
// test for uniqueness
if !isNewPath(absPath, processed) {
continue
}
// mark this absolute path as processed
// - if it's a file, we'll never process it again
// - if it's a dir, we'll never walk it again
processed[absPath] = true
if isFile {
allTemplates = append(allTemplates, absPath)
} else {
matches := []string{}
// Recursively walk down the Templates directory and run all the template file checks
err = godirwalk.Walk(absPath, &godirwalk.Options{
Callback: func(path string, d *godirwalk.Dirent) error {
if !d.IsDir() && strings.HasSuffix(path, ".yaml") {
if isNewPath(path, processed) {
matches = append(matches, path)
processed[path] = true
}
}
return nil
},
ErrorCallback: func(path string, err error) godirwalk.ErrorAction {
return godirwalk.SkipNode
},
Unsorted: true,
})
// directory couldn't be walked
if err != nil {
gologger.Labelf("Could not find templates in directory '%s': %s\n", absPath, err)
continue
}
// couldn't find templates in directory
if len(matches) == 0 {
gologger.Labelf("Error, no templates were found in '%s'.\n", absPath)
continue
}
allTemplates = append(allTemplates, matches...)
}
}
}
2020-06-26 12:37:55 +00:00
2020-08-02 16:33:55 +00:00
// pre-parse all the templates, apply filters
availableTemplates, workflowCount := r.getParsedTemplatesFor(allTemplates, r.options.Severity)
templateCount := len(availableTemplates)
hasWorkflows := workflowCount > 0
// 0 matches means no templates were found in directory
2020-08-02 16:33:55 +00:00
if templateCount == 0 {
gologger.Fatalf("Error, no templates were found.\n")
}
2020-08-02 16:33:55 +00:00
gologger.Infof("Using %s rules (%s templates, %s workflows)",
r.colorizer.Bold(templateCount).String(),
r.colorizer.Bold(templateCount-workflowCount).String(),
r.colorizer.Bold(workflowCount).String())
2020-07-23 18:19:19 +00:00
2020-07-25 21:22:09 +00:00
// precompute total request count
var totalRequests int64 = 0
2020-08-02 16:33:55 +00:00
for _, t := range availableTemplates {
switch t.(type) {
case *templates.Template:
template := t.(*templates.Template)
totalRequests += (template.GetHTTPRequestCount() + template.GetDNSRequestCount()) * r.inputCount
2020-07-26 22:00:06 +00:00
case *workflows.Workflow:
// workflows will dynamically adjust the totals while running, as
// it can't be know in advance which requests will be called
}
}
2020-07-23 18:19:19 +00:00
2020-07-24 16:12:16 +00:00
var (
wgtemplates sync.WaitGroup
results atomicboolean.AtomBool
)
if r.inputCount == 0 {
gologger.Errorf("Could not find any valid input URLs.")
} else if totalRequests > 0 || hasWorkflows {
// tracks global progress and captures stdout/stderr until p.Wait finishes
2020-08-02 16:33:55 +00:00
p := r.progress
p.InitProgressbar(r.inputCount, templateCount, totalRequests)
2020-08-02 16:33:55 +00:00
for _, t := range availableTemplates {
wgtemplates.Add(1)
2020-08-02 16:33:55 +00:00
go func(template interface{}) {
defer wgtemplates.Done()
2020-08-02 16:33:55 +00:00
switch template.(type) {
case *templates.Template:
2020-08-02 16:33:55 +00:00
t := template.(*templates.Template)
for _, request := range t.RequestsDNS {
results.Or(r.processTemplateWithList(p, t, request))
}
2020-08-02 16:33:55 +00:00
for _, request := range t.BulkRequestsHTTP {
results.Or(r.processTemplateWithList(p, t, request))
}
case *workflows.Workflow:
2020-08-02 16:33:55 +00:00
workflow := template.(*workflows.Workflow)
r.ProcessWorkflowWithList(p, workflow)
2020-06-26 12:37:55 +00:00
}
2020-08-02 16:33:55 +00:00
}(t)
}
2020-07-24 16:12:16 +00:00
wgtemplates.Wait()
p.Wait()
}
2020-07-23 18:19:19 +00:00
2020-07-24 16:12:16 +00:00
if !results.Get() {
if r.output != nil {
outputFile := r.output.Name()
r.output.Close()
os.Remove(outputFile)
}
gologger.Infof("No results found. Happy hacking!")
}
return
}
2020-07-23 18:19:19 +00:00
// processTemplateWithList processes a template and runs the enumeration on all the targets
func (r *Runner) processTemplateWithList(p progress.IProgress, template *templates.Template, request interface{}) bool {
// Display the message for the template
message := fmt.Sprintf("[%s] Loaded template %s (@%s)", template.ID, template.Info.Name, template.Info.Author)
if template.Info.Severity != "" {
message += " [" + template.Info.Severity + "]"
}
gologger.Infof("%s\n", message)
2020-04-04 12:51:05 +00:00
var writer *bufio.Writer
if r.output != nil {
writer = bufio.NewWriter(r.output)
defer writer.Flush()
}
2020-07-16 08:57:28 +00:00
var httpExecuter *executer.HTTPExecuter
var dnsExecuter *executer.DNSExecuter
2020-04-27 18:19:53 +00:00
var err error
2020-07-16 08:57:28 +00:00
// Create an executer based on the request type.
switch value := request.(type) {
case *requests.DNSRequest:
2020-07-16 08:57:28 +00:00
dnsExecuter = executer.NewDNSExecuter(&executer.DNSOptions{
2020-06-22 14:00:01 +00:00
Debug: r.options.Debug,
Template: template,
DNSRequest: value,
Writer: writer,
2020-06-27 14:49:43 +00:00
JSON: r.options.JSON,
ColoredOutput: !r.options.NoColor,
Colorizer: r.colorizer,
Decolorizer: r.decolorizer,
})
2020-07-18 19:42:23 +00:00
case *requests.BulkHTTPRequest:
2020-07-16 08:57:28 +00:00
httpExecuter, err = executer.NewHTTPExecuter(&executer.HTTPOptions{
2020-07-18 19:42:23 +00:00
Debug: r.options.Debug,
Template: template,
BulkHttpRequest: value,
Writer: writer,
Timeout: r.options.Timeout,
Retries: r.options.Retries,
ProxyURL: r.options.ProxyURL,
ProxySocksURL: r.options.ProxySocksURL,
CustomHeaders: r.options.CustomHeaders,
JSON: r.options.JSON,
JSONRequests: r.options.JSONRequests,
2020-07-18 19:42:23 +00:00
CookieReuse: value.CookieReuse,
ColoredOutput: !r.options.NoColor,
Colorizer: r.colorizer,
Decolorizer: r.decolorizer,
})
}
2020-04-27 18:19:53 +00:00
if err != nil {
p.Drop(request.(*requests.BulkHTTPRequest).GetRequestCount())
2020-04-27 18:19:53 +00:00
gologger.Warningf("Could not create http client: %s\n", err)
return false
2020-04-27 18:19:53 +00:00
}
2020-07-25 18:11:46 +00:00
var globalresult atomicboolean.AtomBool
2020-07-24 16:12:16 +00:00
var wg sync.WaitGroup
scanner := bufio.NewScanner(strings.NewReader(r.input))
for scanner.Scan() {
text := scanner.Text()
2020-07-24 16:12:16 +00:00
r.limiter <- struct{}{}
wg.Add(1)
go func(URL string) {
2020-07-24 16:12:16 +00:00
defer wg.Done()
2020-07-16 08:57:28 +00:00
var result executer.Result
2020-07-16 08:57:28 +00:00
if httpExecuter != nil {
2020-07-23 18:19:19 +00:00
result = httpExecuter.ExecuteHTTP(p, URL)
2020-07-25 18:11:46 +00:00
globalresult.Or(result.GotResults)
}
2020-07-16 08:57:28 +00:00
if dnsExecuter != nil {
2020-07-26 14:36:01 +00:00
result = dnsExecuter.ExecuteDNS(p, URL)
2020-07-25 18:11:46 +00:00
globalresult.Or(result.GotResults)
}
2020-07-10 07:04:38 +00:00
if result.Error != nil {
gologger.Warningf("Could not execute step: %s\n", result.Error)
}
2020-07-24 16:12:16 +00:00
<-r.limiter
}(text)
}
2020-07-24 16:12:16 +00:00
wg.Wait()
2020-07-16 08:57:28 +00:00
// See if we got any results from the executers
2020-07-25 18:11:46 +00:00
return globalresult.Get()
}
2020-06-26 08:23:54 +00:00
// ProcessWorkflowWithList coming from stdin or list of targets
func (r *Runner) ProcessWorkflowWithList(p progress.IProgress, workflow *workflows.Workflow) {
2020-07-27 17:36:40 +00:00
var wg sync.WaitGroup
scanner := bufio.NewScanner(strings.NewReader(r.input))
2020-06-26 08:23:54 +00:00
for scanner.Scan() {
text := scanner.Text()
2020-07-27 17:36:40 +00:00
r.limiter <- struct{}{}
wg.Add(1)
go func(URL string) {
defer wg.Done()
if err := r.ProcessWorkflow(p, workflow, text); err != nil {
2020-07-27 17:36:40 +00:00
gologger.Warningf("Could not run workflow for %s: %s\n", text, err)
}
<-r.limiter
}(text)
2020-06-26 08:23:54 +00:00
}
2020-07-27 17:36:40 +00:00
wg.Wait()
2020-06-26 08:23:54 +00:00
}
// ProcessWorkflow towards an URL
func (r *Runner) ProcessWorkflow(p progress.IProgress, workflow *workflows.Workflow, URL string) error {
2020-06-26 08:23:54 +00:00
script := tengo.NewScript([]byte(workflow.Logic))
2020-07-10 07:04:38 +00:00
script.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
2020-07-16 14:32:42 +00:00
var jar *cookiejar.Jar
if workflow.CookieReuse {
var err error
jar, err = cookiejar.New(nil)
if err != nil {
return err
}
}
2020-06-26 08:23:54 +00:00
for name, value := range workflow.Variables {
var writer *bufio.Writer
if r.output != nil {
writer = bufio.NewWriter(r.output)
defer writer.Flush()
}
2020-06-29 12:13:08 +00:00
// Check if the template is an absolute path or relative path.
// If the path is absolute, use it. Otherwise,
if r.isRelative(value) {
newPath, err := r.resolvePath(value)
if err != nil {
newPath, err = r.resolvePathWithBaseFolder(filepath.Dir(workflow.GetPath()), value)
if err != nil {
return err
}
2020-06-29 12:13:08 +00:00
}
value = newPath
2020-06-26 08:23:54 +00:00
}
2020-06-29 12:13:08 +00:00
// Single yaml provided
var templatesList []*workflows.Template
if strings.HasSuffix(value, ".yaml") {
t, err := templates.Parse(value)
if err != nil {
return err
}
2020-07-26 22:00:06 +00:00
template := &workflows.Template{Progress: p}
2020-07-18 19:42:23 +00:00
if len(t.BulkRequestsHTTP) > 0 {
2020-07-16 08:57:28 +00:00
template.HTTPOptions = &executer.HTTPOptions{
2020-06-29 12:13:08 +00:00
Debug: r.options.Debug,
Writer: writer,
Template: t,
Timeout: r.options.Timeout,
Retries: r.options.Retries,
ProxyURL: r.options.ProxyURL,
ProxySocksURL: r.options.ProxySocksURL,
CustomHeaders: r.options.CustomHeaders,
2020-07-16 14:32:42 +00:00
CookieJar: jar,
ColoredOutput: !r.options.NoColor,
Colorizer: r.colorizer,
Decolorizer: r.decolorizer,
2020-06-29 12:13:08 +00:00
}
} else if len(t.RequestsDNS) > 0 {
2020-07-16 08:57:28 +00:00
template.DNSOptions = &executer.DNSOptions{
2020-06-29 12:13:08 +00:00
Debug: r.options.Debug,
Template: t,
Writer: writer,
ColoredOutput: !r.options.NoColor,
Colorizer: r.colorizer,
Decolorizer: r.decolorizer,
2020-06-29 12:13:08 +00:00
}
}
if template.DNSOptions != nil || template.HTTPOptions != nil {
templatesList = append(templatesList, template)
}
} else {
matches := []string{}
err := godirwalk.Walk(value, &godirwalk.Options{
Callback: func(path string, d *godirwalk.Dirent) error {
if !d.IsDir() && strings.HasSuffix(path, ".yaml") {
matches = append(matches, path)
}
return nil
},
ErrorCallback: func(path string, err error) godirwalk.ErrorAction {
return godirwalk.SkipNode
},
Unsorted: true,
})
if err != nil {
return err
}
// 0 matches means no templates were found in directory
if len(matches) == 0 {
return errors.New("no match found in the directory")
}
for _, match := range matches {
t, err := templates.Parse(match)
if err != nil {
return err
}
2020-07-26 22:00:06 +00:00
template := &workflows.Template{Progress: p}
2020-07-18 19:42:23 +00:00
if len(t.BulkRequestsHTTP) > 0 {
2020-07-16 08:57:28 +00:00
template.HTTPOptions = &executer.HTTPOptions{
2020-06-29 12:13:08 +00:00
Debug: r.options.Debug,
Writer: writer,
Template: t,
Timeout: r.options.Timeout,
Retries: r.options.Retries,
ProxyURL: r.options.ProxyURL,
ProxySocksURL: r.options.ProxySocksURL,
CustomHeaders: r.options.CustomHeaders,
2020-07-16 14:32:42 +00:00
CookieJar: jar,
2020-06-29 12:13:08 +00:00
}
} else if len(t.RequestsDNS) > 0 {
2020-07-16 08:57:28 +00:00
template.DNSOptions = &executer.DNSOptions{
2020-06-29 12:13:08 +00:00
Debug: r.options.Debug,
Template: t,
Writer: writer,
}
}
if template.DNSOptions != nil || template.HTTPOptions != nil {
templatesList = append(templatesList, template)
}
}
2020-06-26 13:10:42 +00:00
}
2020-06-29 12:13:08 +00:00
script.Add(name, &workflows.NucleiVar{Templates: templatesList, URL: URL})
2020-06-26 08:23:54 +00:00
}
_, err := script.RunContext(context.Background())
if err != nil {
gologger.Errorf("Could not execute workflow '%s': %s\n", workflow.ID, err)
return err
}
return nil
}
2020-06-26 12:37:55 +00:00
2020-06-30 16:57:52 +00:00
func (r *Runner) parse(file string) (interface{}, error) {
2020-06-26 12:37:55 +00:00
// check if it's a template
2020-06-30 16:57:52 +00:00
template, errTemplate := templates.Parse(file)
2020-06-26 12:37:55 +00:00
if errTemplate == nil {
2020-06-30 16:57:52 +00:00
return template, nil
2020-06-26 12:37:55 +00:00
}
// check if it's a workflow
2020-06-30 16:57:52 +00:00
workflow, errWorkflow := workflows.Parse(file)
2020-06-26 12:37:55 +00:00
if errWorkflow == nil {
2020-06-30 16:57:52 +00:00
return workflow, nil
2020-06-26 12:37:55 +00:00
}
2020-06-30 16:57:52 +00:00
if errTemplate != nil {
return nil, errTemplate
}
if errWorkflow != nil {
return nil, errWorkflow
}
return nil, errors.New("unknown error occured")
2020-06-26 12:37:55 +00:00
}