RES-84 # Improve Nuclei CLI interface (WIP)

* removed the generic isEmpty implementation
This commit is contained in:
forgedhallpass 2021-08-03 14:51:34 +03:00
parent 68a6d394e7
commit 2f162e859e
13 changed files with 52 additions and 155 deletions

View File

@ -123,7 +123,7 @@ func New(options *types.Options) (*Runner, error) {
os.Exit(0) os.Exit(0)
} }
if (utils.IsEmpty(options.Templates) || !options.NewTemplates || (utils.IsEmpty(options.Targets, options.Target) && utils.IsNotEmpty(options.Stdin))) && options.UpdateTemplates { if (len(options.Templates) == 0 || !options.NewTemplates || (options.Targets == "" && !options.Stdin && options.Target == "")) && options.UpdateTemplates {
os.Exit(0) os.Exit(0)
} }
hm, err := hybrid.New(hybrid.DefaultDiskOptions) hm, err := hybrid.New(hybrid.DefaultDiskOptions)
@ -211,7 +211,7 @@ func New(options *types.Options) (*Runner, error) {
// create project file if requested or load existing one // create project file if requested or load existing one
if options.Project { if options.Project {
var projectFileErr error var projectFileErr error
runner.projectFile, projectFileErr = projectfile.New(&projectfile.Options{Path: options.ProjectPath, Cleanup: utils.IsEmpty(options.ProjectPath)}) runner.projectFile, projectFileErr = projectfile.New(&projectfile.Options{Path: options.ProjectPath, Cleanup: utils.IsBlank(options.ProjectPath)})
if projectFileErr != nil { if projectFileErr != nil {
return nil, projectFileErr return nil, projectFileErr
} }
@ -354,10 +354,10 @@ func (r *Runner) RunEnumeration() error {
if r.interactsh != nil { if r.interactsh != nil {
gologger.Info().Msgf("Using Interactsh Server %s", r.options.InteractshURL) gologger.Info().Msgf("Using Interactsh Server %s", r.options.InteractshURL)
} }
if utils.IsNotEmpty(store.Templates()) { if len(store.Templates()) > 0 {
gologger.Info().Msgf("Templates loaded: %d (New: %d)", len(store.Templates()), r.countNewTemplates()) gologger.Info().Msgf("Templates loaded: %d (New: %d)", len(store.Templates()), r.countNewTemplates())
} }
if utils.IsNotEmpty(store.Workflows()) { if len(store.Workflows()) > 0 {
gologger.Info().Msgf("Workflows loaded: %d", len(store.Workflows())) gologger.Info().Msgf("Workflows loaded: %d", len(store.Workflows()))
} }
@ -367,8 +367,8 @@ func (r *Runner) RunEnumeration() error {
var unclusteredRequests int64 var unclusteredRequests int64
for _, template := range store.Templates() { for _, template := range store.Templates() {
// workflows will dynamically adjust the totals while running, as // workflows will dynamically adjust the totals while running, as
// it can't be know in advance which requests will be called // it can't be known in advance which requests will be called
if utils.IsNotEmpty(template.Workflows) { if len(template.Workflows) > 0 {
continue continue
} }
unclusteredRequests += int64(template.TotalRequests) * r.inputCount unclusteredRequests += int64(template.TotalRequests) * r.inputCount
@ -419,7 +419,7 @@ func (r *Runner) RunEnumeration() error {
var totalRequests int64 var totalRequests int64
for _, t := range finalTemplates { for _, t := range finalTemplates {
if utils.IsNotEmpty(t.Workflows) { if len(t.Workflows) > 0 {
continue continue
} }
totalRequests += int64(t.TotalRequests) * r.inputCount totalRequests += int64(t.TotalRequests) * r.inputCount

View File

@ -5,7 +5,6 @@ import (
"strings" "strings"
"github.com/projectdiscovery/nuclei/v2/internal/severity" "github.com/projectdiscovery/nuclei/v2/internal/severity"
"github.com/projectdiscovery/nuclei/v2/pkg/utils"
) )
// TagFilter is used to filter nuclei templates for tag based execution // TagFilter is used to filter nuclei templates for tag based execution
@ -46,7 +45,7 @@ func (tagFilter *TagFilter) Match(templateTags, templateAuthors []string, templa
return false, nil return false, nil
} }
if utils.IsNotEmpty(tagFilter.severities) { if len(tagFilter.severities) > 0 {
if _, ok := tagFilter.severities[templateSeverity]; !ok { if _, ok := tagFilter.severities[templateSeverity]; !ok {
return false, nil return false, nil
} }
@ -56,7 +55,7 @@ func (tagFilter *TagFilter) Match(templateTags, templateAuthors []string, templa
} }
func isAuthorMatch(templateAuthors []string, tagFilter *TagFilter) bool { func isAuthorMatch(templateAuthors []string, tagFilter *TagFilter) bool {
if utils.IsEmpty(tagFilter.authors) { if len(tagFilter.authors) == 0 {
return true return true
} }
@ -70,7 +69,7 @@ func isAuthorMatch(templateAuthors []string, tagFilter *TagFilter) bool {
} }
func isTagMatch(templateTags []string, tagFilter *TagFilter) bool { func isTagMatch(templateTags []string, tagFilter *TagFilter) bool {
if utils.IsEmpty(tagFilter.allowedTags) { if len(tagFilter.allowedTags) == 0 {
return true return true
} }
@ -101,7 +100,7 @@ func (tagFilter *TagFilter) MatchWithWorkflowTags(templateTags, templateAuthors
} }
} }
if utils.IsNotEmpty(workflowAllowedTagMap) { // TODO review, does not seem to make sense if len(workflowAllowedTagMap) > 0 { // TODO review, does not seem to make sense
for _, templateTag := range templateTags { for _, templateTag := range templateTags {
if _, ok := workflowAllowedTagMap[templateTag]; !ok { if _, ok := workflowAllowedTagMap[templateTag]; !ok {
return false, nil return false, nil
@ -109,7 +108,7 @@ func (tagFilter *TagFilter) MatchWithWorkflowTags(templateTags, templateAuthors
} }
} }
if utils.IsNotEmpty(tagFilter.authors) { if len(tagFilter.authors) > 0 {
for _, templateAuthor := range templateAuthors { for _, templateAuthor := range templateAuthors {
if _, ok := tagFilter.authors[templateAuthor]; !ok { if _, ok := tagFilter.authors[templateAuthor]; !ok {
return false, nil return false, nil
@ -117,7 +116,7 @@ func (tagFilter *TagFilter) MatchWithWorkflowTags(templateTags, templateAuthors
} }
} }
if utils.IsNotEmpty(tagFilter.severities) { if len(tagFilter.severities) > 0 {
if _, ok := tagFilter.severities[templateSeverity]; !ok { if _, ok := tagFilter.severities[templateSeverity]; !ok {
return false, nil return false, nil
} }

View File

@ -10,7 +10,6 @@ import (
"github.com/projectdiscovery/nuclei/v2/pkg/parsers" "github.com/projectdiscovery/nuclei/v2/pkg/parsers"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols" "github.com/projectdiscovery/nuclei/v2/pkg/protocols"
"github.com/projectdiscovery/nuclei/v2/pkg/templates" "github.com/projectdiscovery/nuclei/v2/pkg/templates"
"github.com/projectdiscovery/nuclei/v2/pkg/utils"
) )
// Config contains the configuration options for the loader // Config contains the configuration options for the loader
@ -61,7 +60,7 @@ func New(config *Config) (*Store, error) {
} }
// Handle a case with no templates or workflows, where we use base directory // Handle a case with no templates or workflows, where we use base directory
if utils.IsEmpty(config.Templates, config.Workflows) { if len(config.Templates) == 0 && len(config.Workflows) == 0 {
config.Templates = append(config.Templates, config.TemplatesDirectory) config.Templates = append(config.Templates, config.TemplatesDirectory)
} }
store.finalTemplates = append(store.finalTemplates, config.Templates...) store.finalTemplates = append(store.finalTemplates, config.Templates...)

View File

@ -1,10 +1,11 @@
package model package model
import ( import (
"fmt"
"github.com/projectdiscovery/nuclei/v2/pkg/utils"
"strings" "strings"
"github.com/projectdiscovery/nuclei/v2/internal/severity" "github.com/projectdiscovery/nuclei/v2/internal/severity"
"github.com/projectdiscovery/nuclei/v2/pkg/utils"
) )
type Info struct { type Info struct {
@ -16,12 +17,14 @@ type Info struct {
SeverityHolder severity.SeverityHolder `yaml:"severity"` SeverityHolder severity.SeverityHolder `yaml:"severity"`
} }
// StringSlice represents a single (in-lined) or multiple string value(s).
// The unmarshaller does not automatically convert in-lined strings to []string, hence the interface{} type is required.
type StringSlice struct { type StringSlice struct {
Value interface{} Value interface{}
} }
func (stringSlice *StringSlice) IsEmpty() bool { func (stringSlice *StringSlice) IsEmpty() bool {
return utils.IsEmpty(stringSlice.Value) return len(stringSlice.ToSlice()) == 0
} }
func (stringSlice StringSlice) ToSlice() []string { func (stringSlice StringSlice) ToSlice() []string {
@ -32,8 +35,9 @@ func (stringSlice StringSlice) ToSlice() []string {
return value return value
case nil: case nil:
return []string{} return []string{}
default:
panic(fmt.Sprintf("Unexpected StringSlice type: '%T'", value))
} }
panic("Illegal State: StringSlice holds non-string value(s)")
} }
func (stringSlice *StringSlice) UnmarshalYAML(unmarshal func(interface{}) error) error { func (stringSlice *StringSlice) UnmarshalYAML(unmarshal func(interface{}) error) error {
@ -44,7 +48,7 @@ func (stringSlice *StringSlice) UnmarshalYAML(unmarshal func(interface{}) error)
result := make([]string, len(marshalledSlice)) result := make([]string, len(marshalledSlice))
for _, value := range marshalledSlice { for _, value := range marshalledSlice {
result = append(result, strings.ToLower(strings.TrimSpace(value))) result = append(result, strings.ToLower(strings.TrimSpace(value))) // TODO do we need to introduce RawStringSlice and/or NormalizedStringSlices?
} }
stringSlice.Value = result stringSlice.Value = result
return nil return nil
@ -65,7 +69,7 @@ func marshalStringToSlice(unmarshal func(interface{}) error) ([]string, error) {
var result []string var result []string
if len(marshalledValuesAsSlice) > 0 { if len(marshalledValuesAsSlice) > 0 {
result = marshalledValuesAsSlice result = marshalledValuesAsSlice
} else if utils.IsNotEmpty(marshalledValueAsString) { } else if utils.IsNotBlank(marshalledValueAsString) {
result = strings.Split(marshalledValueAsString, ",") result = strings.Split(marshalledValueAsString, ",")
} else { } else {
result = []string{} result = []string{}

View File

@ -28,7 +28,7 @@ func Load(templatePath string, isWorkflow bool, workflowTags []string, tagFilter
return false, validationError return false, validationError
} }
if utils.IsNotEmpty(template.Workflows) { if len(template.Workflows) > 0 {
if isWorkflow { if isWorkflow {
return true, nil // if a workflow is declared and this template is a workflow, then load return true, nil // if a workflow is declared and this template is a workflow, then load
} else { //nolint:indent-error-flow,revive // preferred: readability and extensibility } else { //nolint:indent-error-flow,revive // preferred: readability and extensibility
@ -48,7 +48,7 @@ func isInfoMetadataMatch(tagFilter *filter.TagFilter, templateInfo *model.Info,
var match bool var match bool
var err error var err error
if utils.IsEmpty(workflowTags) { if len(workflowTags) == 0 {
match, err = tagFilter.Match(templateTags, templateAuthors, templateSeverity) match, err = tagFilter.Match(templateTags, templateAuthors, templateSeverity)
} else { } else {
match, err = tagFilter.MatchWithWorkflowTags(templateTags, templateAuthors, templateSeverity, workflowTags) match, err = tagFilter.MatchWithWorkflowTags(templateTags, templateAuthors, templateSeverity, workflowTags)
@ -62,16 +62,15 @@ func isInfoMetadataMatch(tagFilter *filter.TagFilter, templateInfo *model.Info,
} }
func validateMandatoryInfoFields(info *model.Info) error { func validateMandatoryInfoFields(info *model.Info) error {
if utils.IsEmpty(&info) { if &info == nil {
return fmt.Errorf(mandatoryFieldMissingTemplate, "info") return fmt.Errorf(mandatoryFieldMissingTemplate, "info")
} }
if utils.IsEmpty(&info.Name) { if utils.IsBlank(info.Name) {
return fmt.Errorf(mandatoryFieldMissingTemplate, "name") return fmt.Errorf(mandatoryFieldMissingTemplate, "name")
} }
authors := info.Authors.ToSlice() if info.Authors.IsEmpty() {
if utils.IsEmpty(&authors) {
return fmt.Errorf(mandatoryFieldMissingTemplate, "author") return fmt.Errorf(mandatoryFieldMissingTemplate, "author")
} }
return nil return nil

View File

@ -62,7 +62,7 @@ func (i *Exporter) Export(event *output.ResultEvent) error {
sarifSeverity := getSarifSeverity(event) sarifSeverity := getSarifSeverity(event)
var ruleName string var ruleName string
if utils.IsNotEmpty(event.Info.Name) { if utils.IsNotBlank(event.Info.Name) {
ruleName = event.Info.Name ruleName = event.Info.Name
} }
@ -74,7 +74,7 @@ func (i *Exporter) Export(event *output.ResultEvent) error {
} }
var ruleDescription string var ruleDescription string
if utils.IsNotEmpty(event.Info.Description) { if utils.IsNotBlank(event.Info.Description) {
ruleDescription = event.Info.Description ruleDescription = event.Info.Description
} }

View File

@ -8,7 +8,6 @@ import (
"github.com/projectdiscovery/nuclei/v2/pkg/output" "github.com/projectdiscovery/nuclei/v2/pkg/output"
"github.com/projectdiscovery/nuclei/v2/pkg/types" "github.com/projectdiscovery/nuclei/v2/pkg/types"
"github.com/projectdiscovery/nuclei/v2/pkg/utils"
) )
// Summary returns a formatted built one line summary of the event // Summary returns a formatted built one line summary of the event
@ -125,11 +124,12 @@ func MarkdownDescription(event *output.ResultEvent) string {
} }
} }
referenceValue := event.Info.Reference.Value reference := event.Info.Reference
if utils.IsNotEmpty(referenceValue) { if !reference.IsEmpty() {
builder.WriteString("\nReference: \n") builder.WriteString("\nReference: \n")
switch value := referenceValue.(type) { // TODO revisit // TODO remove the code duplication: format.go <-> jira.go
switch value := reference.Value.(type) {
case string: case string:
if !strings.HasPrefix(value, "-") { if !strings.HasPrefix(value, "-") {
builder.WriteString("- ") builder.WriteString("- ")

View File

@ -15,7 +15,6 @@ import (
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/trackers/github" "github.com/projectdiscovery/nuclei/v2/pkg/reporting/trackers/github"
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/trackers/gitlab" "github.com/projectdiscovery/nuclei/v2/pkg/reporting/trackers/gitlab"
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/trackers/jira" "github.com/projectdiscovery/nuclei/v2/pkg/reporting/trackers/jira"
"github.com/projectdiscovery/nuclei/v2/pkg/utils"
) )
// Options is a configuration file for nuclei reporting module // Options is a configuration file for nuclei reporting module

View File

@ -12,7 +12,6 @@ import (
"github.com/projectdiscovery/nuclei/v2/pkg/output" "github.com/projectdiscovery/nuclei/v2/pkg/output"
"github.com/projectdiscovery/nuclei/v2/pkg/reporting/format" "github.com/projectdiscovery/nuclei/v2/pkg/reporting/format"
"github.com/projectdiscovery/nuclei/v2/pkg/types" "github.com/projectdiscovery/nuclei/v2/pkg/types"
"github.com/projectdiscovery/nuclei/v2/pkg/utils"
) )
// Integration is a client for a issue tracker integration // Integration is a client for a issue tracker integration
@ -186,11 +185,13 @@ func jiraFormatDescription(event *output.ResultEvent) string {
builder.WriteString("\n{code}\n") builder.WriteString("\n{code}\n")
} }
} }
referenceValue := event.Info.Reference.Value
if utils.IsNotEmpty(referenceValue) { reference := event.Info.Reference
if !reference.IsEmpty() {
builder.WriteString("\nReference: \n") builder.WriteString("\nReference: \n")
switch v := referenceValue.(type) { // TODO revisit // TODO remove the code duplication: format.go <-> jira.go
switch v := reference.Value.(type) {
case string: case string:
if !strings.HasPrefix(v, "-") { if !strings.HasPrefix(v, "-") {
builder.WriteString("- ") builder.WriteString("- ")

View File

@ -39,10 +39,10 @@ func Parse(filePath string, options protocols.ExecuterOptions) (*Template, error
return nil, err return nil, err
} }
if utils.IsEmpty(template.Info.Name) { if utils.IsBlank(template.Info.Name) {
return nil, errors.New("no template name field provided") return nil, errors.New("no template name field provided")
} }
if utils.IsEmpty(template.Info.Authors) { if template.Info.Authors.IsEmpty() {
return nil, errors.New("no template author field provided") return nil, errors.New("no template author field provided")
} }
@ -52,12 +52,12 @@ func Parse(filePath string, options protocols.ExecuterOptions) (*Template, error
options.TemplatePath = filePath options.TemplatePath = filePath
// If no requests, and it is also not a workflow, return error. // If no requests, and it is also not a workflow, return error.
if utils.IsEmpty(template.RequestsDNS, template.RequestsHTTP, template.RequestsFile, template.RequestsNetwork, template.RequestsHeadless, template.Workflows) { if len(template.RequestsDNS)+len(template.RequestsHTTP)+len(template.RequestsFile)+len(template.RequestsNetwork)+len(template.RequestsHeadless)+len(template.Workflows) == 0 {
return nil, fmt.Errorf("no requests defined for %s", template.ID) return nil, fmt.Errorf("no requests defined for %s", template.ID)
} }
// Compile the workflow request // Compile the workflow request
if utils.IsNotEmpty(template.Workflows) { if len(template.Workflows) > 0 {
compiled := &template.Workflow compiled := &template.Workflow
compileWorkflow(&options, compiled, options.WorkflowLoader) compileWorkflow(&options, compiled, options.WorkflowLoader)

View File

@ -4,7 +4,6 @@ import (
"github.com/projectdiscovery/gologger" "github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v2/pkg/model" "github.com/projectdiscovery/nuclei/v2/pkg/model"
"github.com/projectdiscovery/nuclei/v2/pkg/protocols" "github.com/projectdiscovery/nuclei/v2/pkg/protocols"
"github.com/projectdiscovery/nuclei/v2/pkg/utils"
"github.com/projectdiscovery/nuclei/v2/pkg/workflows" "github.com/projectdiscovery/nuclei/v2/pkg/workflows"
) )
@ -45,13 +44,13 @@ func parseWorkflow(workflow *workflows.WorkflowTemplate, options *protocols.Exec
return nil return nil
} }
// parseWorkflowTemplate parses a workflow template creating an executor // parseWorkflowTemplate parses a workflow template creating an executer
func parseWorkflowTemplate(workflow *workflows.WorkflowTemplate, options *protocols.ExecuterOptions, loader model.WorkflowLoader, noValidate bool) error { func parseWorkflowTemplate(workflow *workflows.WorkflowTemplate, options *protocols.ExecuterOptions, loader model.WorkflowLoader, noValidate bool) error {
var paths []string var paths []string
workflowTags := workflow.Tags.ToSlice() workflowTags := workflow.Tags
if utils.IsNotEmpty(workflowTags) { if !workflowTags.IsEmpty() {
paths = loader.ListTags(workflowTags) paths = loader.ListTags(workflowTags.ToSlice())
} else { } else {
paths = loader.ListTemplates([]string{workflow.Template}, noValidate) paths = loader.ListTemplates([]string{workflow.Template}, noValidate)
} }
@ -76,7 +75,7 @@ func parseWorkflowTemplate(workflow *workflows.WorkflowTemplate, options *protoc
continue continue
} }
if template.Executer == nil { if template.Executer == nil {
gologger.Warning().Msgf("Could not parse workflow template %s: no executor found\n", path) gologger.Warning().Msgf("Could not parse workflow template %s: no executer found\n", path)
continue continue
} }
workflow.Executers = append(workflow.Executers, &workflows.ProtocolExecuterPair{ workflow.Executers = append(workflow.Executers, &workflows.ProtocolExecuterPair{

View File

@ -1,44 +1,13 @@
package utils package utils
import ( import (
"reflect"
"strings" "strings"
) )
func isEmpty(value interface{}) bool { func IsBlank(value string) bool {
if value == nil { return strings.TrimSpace(value) == ""
return true
}
reflectValue := reflect.ValueOf(value)
actualValueInterface := reflectValue.Interface()
// nolint:exhaustive //default branch handles other cases
switch reflect.TypeOf(value).Kind() {
case reflect.String:
reflectedValue := actualValueInterface.(string)
return strings.TrimSpace(reflectedValue) == ""
case reflect.Slice, reflect.Array, reflect.Map:
return reflectValue.Len() == 0
case reflect.Int32:
return IsEmpty(string(actualValueInterface.(rune)))
default:
if reflectValue.IsZero() {
return true
}
return false
}
} }
func IsEmpty(value ...interface{}) bool { func IsNotBlank(value string) bool {
for _, current := range value { return !IsBlank(value)
if IsNotEmpty(current) {
return false
}
}
return true
}
func IsNotEmpty(value interface{}) bool {
return !isEmpty(value)
} }

View File

@ -1,72 +0,0 @@
package utils
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
//nolint:scopelint //false-positive
func TestIsEmpty(t *testing.T) {
testCases := [...][2]interface{}{
{"", true},
{' ', true},
{'\t', true},
{'\n', true},
{" ", true},
{"\n", true},
{"\t", true},
{0, true},
{[]string{}, true},
{[0]string{}, true},
{[...]string{}, true},
{[]int{}, true},
{[0]int{}, true},
{[...]int{}, true},
{interface{}(nil), true},
{[]struct{}(nil), true},
{[]interface{}(nil), true},
{map[string]interface{}{}, true},
{nil, true},
{'a', false},
{1, false},
{3.14, false},
{" test ", false},
{[]string{"a"}, false},
{[...]string{"a"}, false},
{[2]string{"a", "b"}, false},
{[]int{1, 2}, false},
{[...]int{1, 2}, false},
{struct{ a string }{"a"}, false},
{&struct{ a string }{"a"}, false},
{[]struct{ a string }{{"b"}, {"b"}}, false},
{map[string]interface{}{"a": 13}, false},
}
for index, testCase := range testCases {
t.Run(fmt.Sprintf("%v # %d", testCase[0], index), func(t *testing.T) {
assert.Equal(t, testCase[1], IsEmpty(testCase[0]))
})
}
}
func TestVariadicIsEmpty(t *testing.T) {
testVariadicIsEmpty := func(expected bool, value ...interface{}) {
t.Run(fmt.Sprintf("%v", value), func(testCase *testing.T) {
assert.Equal(testCase, expected, IsEmpty(value...))
})
}
testVariadicIsEmpty(false, [2]int{1, 2}, [0]int{})
testVariadicIsEmpty(false, [0]int{}, [2]int{1, 2})
testVariadicIsEmpty(false, [0]int{}, " abc ")
testVariadicIsEmpty(false, [0]int{}, []string{}, 123)
testVariadicIsEmpty(false, [0]int{}, []string{}, []string{"a"})
testVariadicIsEmpty(false, [0]int{}, map[string]int{"a": 123}, map[string]interface{}{"b": "c"})
testVariadicIsEmpty(true, [0]int{}, "")
testVariadicIsEmpty(true, [0]int{}, []string{})
testVariadicIsEmpty(true, [0]int{}, []string{}, 0)
}