Skip to content

Commit

Permalink
Added a filter to the artifact search screen (#2639)
Browse files Browse the repository at this point in the history
  • Loading branch information
scudette committed Apr 22, 2023
1 parent 61e015c commit e9aa032
Show file tree
Hide file tree
Showing 19 changed files with 338 additions and 109 deletions.
4 changes: 2 additions & 2 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -709,9 +709,9 @@ func (self *ApiServer) GetArtifacts(
ctx, org_config_obj, in.ReportType, in.NumberOfResults)
}

terms := strings.Split(in.SearchTerm, " ")
result, err := searchArtifact(
ctx, org_config_obj, terms, in.Type, in.NumberOfResults, in.Fields)
ctx, org_config_obj, in.SearchTerm,
in.Type, in.NumberOfResults, in.Fields)
return result, Status(self.verbose, err)
}

Expand Down
257 changes: 197 additions & 60 deletions api/artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,56 +188,202 @@ func getReportArtifacts(
return result, nil
}

func searchArtifact(
ctx context.Context,
config_obj *config_proto.Config,
terms []string,
artifact_type string,
number_of_results uint64, fields *api_proto.FieldSelector) (
*artifacts_proto.ArtifactDescriptors, error) {
type matchPlan struct {
// These must match against the artifact name
name_regex []*regexp.Regexp

if config_obj.GUI == nil {
return nil, InvalidStatus("GUI not configured")
// These must match against the artifact preconditions
precondition_regex []*regexp.Regexp

tool_regex []*regexp.Regexp

// Acceptable types
types []string

builtin *bool
}

func (self *matchPlan) matchDescOrName(artifact *artifacts_proto.Artifact) bool {
// If no name regexp are specified we do not reject based on name.
if len(self.name_regex) == 0 {
return true
}

name_filter_regexp := config_obj.GUI.ArtifactSearchFilter
if name_filter_regexp == "" {
name_filter_regexp = "."
// All regex must match the same artifact - either in the name or
// description.
matches := 0
for _, re := range self.name_regex {
if re.MatchString(artifact.Name) {
matches++
} else if re.MatchString(artifact.Description) {
matches++
}
}
name_filter := regexp.MustCompile(name_filter_regexp)
return matches == len(self.name_regex)
}

artifact_type = strings.ToLower(artifact_type)
func (self *matchPlan) matchTool(artifact *artifacts_proto.Artifact) bool {
if len(self.tool_regex) == 0 {
return true
}

if number_of_results == 0 {
number_of_results = 1000
if len(artifact.Tools) == 0 {
return false
}

result := &artifacts_proto.ArtifactDescriptors{}
regexes := []*regexp.Regexp{}
for _, term := range terms {
if len(term) <= 2 {
continue
for _, re := range self.tool_regex {
for _, t := range artifact.Tools {
if re.MatchString(t.Name) {
return true
}
}
}
return false
}

re, err := regexp.Compile("(?i)" + term)
if err == nil {
regexes = append(regexes, re)
// Preconditions can exist at the artifact level or at each source.
func (self *matchPlan) matchPreconditions(artifact *artifacts_proto.Artifact) bool {
if len(self.precondition_regex) == 0 {
return true
}

for _, re := range self.precondition_regex {
if artifact.Precondition != "" &&
re.MatchString(artifact.Precondition) {
return true
}
for _, s := range artifact.Sources {
if s.Precondition != "" &&
re.MatchString(s.Precondition) {
return true
}
}
}
return false
}

func (self *matchPlan) matchBuiltin(artifact *artifacts_proto.Artifact) bool {
if self.builtin == nil {
return true
}

if len(regexes) == 0 {
return result, nil
if *self.builtin {
return artifact.BuiltIn
}
return !artifact.BuiltIn
}

matcher := func(text string, regexes []*regexp.Regexp) bool {
for _, re := range regexes {
if re.FindString(text) == "" {
return false
func (self *matchPlan) matchType(artifact *artifacts_proto.Artifact) bool {
if len(self.types) > 0 {
for _, t := range self.types {
if strings.ToLower(artifact.Type) == t {
return true
}
}
return true
return false
}
return true
}

// All conditions must match
func (self *matchPlan) matchArtifact(artifact *artifacts_proto.Artifact) bool {
if !self.matchType(artifact) {
return false
}

if !self.matchDescOrName(artifact) {
return false
}

if !self.matchPreconditions(artifact) {
return false
}

if !self.matchBuiltin(artifact) {
return false
}

if !self.matchTool(artifact) {
return false
}

return true
}

func prepareMatchPlan(search string) *matchPlan {
result := &matchPlan{}
// Tokenise the search expression into search terms:
for _, token := range strings.Split(search, " ") {
if token == "" {
continue
}

parts := strings.SplitN(token, ":", 2)
if len(parts) == 2 {
verb := parts[0]
term := parts[1]
switch verb {
case "type":
result.types = append(result.types,
strings.ToLower(term))
continue

case "precondition":
re, err := regexp.Compile("(?i)" + term)
if err == nil {
result.precondition_regex = append(
result.precondition_regex, re)
}
continue

case "tool":
re, err := regexp.Compile("(?i)" + term)
if err == nil {
result.tool_regex = append(
result.tool_regex, re)
}
continue

case "builtin":
value := false
if term == "yes" {
value = true
}
result.builtin = &value
continue
}
}
re, err := regexp.Compile("(?i)" + token)
if err == nil {
result.name_regex = append(
result.name_regex, re)
}
}
return result
}

func searchArtifact(
ctx context.Context,
config_obj *config_proto.Config,
search_term string,
artifact_type string,
number_of_results uint64, fields *api_proto.FieldSelector) (
*artifacts_proto.ArtifactDescriptors, error) {

if config_obj.GUI == nil {
return nil, InvalidStatus("GUI not configured")
}

matcher := prepareMatchPlan(search_term)
if artifact_type != "" {
matcher.types = append(matcher.types, strings.ToLower(artifact_type))
}

if number_of_results == 0 {
number_of_results = 1000
}

result := &artifacts_proto.ArtifactDescriptors{}
manager, err := services.GetRepositoryManager(config_obj)
if err != nil {
return nil, Status(config_obj.Verbose, err)
Expand All @@ -253,40 +399,31 @@ func searchArtifact(
}

for _, name := range names {
if name_filter.FindString(name) == "" {
artifact, pres := repository.Get(ctx, config_obj, name)
if !pres {
continue
}

artifact, pres := repository.Get(ctx, config_obj, name)
if pres {
// Skip non matching types
if artifact_type != "" &&
artifact.Type != artifact_type {
continue
}
if matcher.matchArtifact(artifact) {
if fields == nil {
result.Items = append(result.Items, artifact)
} else {
// Send back minimal information about the
// artifacts
new_item := &artifacts_proto.Artifact{}
if fields.Name {
new_item.Name = artifact.Name
new_item.BuiltIn = artifact.BuiltIn
}

if matcher(artifact.Description, regexes) ||
matcher(artifact.Name, regexes) {
if fields == nil {
result.Items = append(result.Items, artifact)
} else {
// Send back minimal information about the
// artifacts
new_item := &artifacts_proto.Artifact{}
if fields.Name {
new_item.Name = artifact.Name
new_item.BuiltIn = artifact.BuiltIn
}

if fields.Description {
new_item.Description = artifact.Description
}
if fields.Type {
new_item.Type = artifact.Type
}

result.Items = append(result.Items, new_item)
if fields.Description {
new_item.Description = artifact.Description
}
if fields.Type {
new_item.Type = artifact.Type
}

result.Items = append(result.Items, new_item)
}
}

Expand Down
16 changes: 16 additions & 0 deletions gui/velociraptor/src/components/artifacts/artifacts.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,19 @@ pre {
span.user-edit {
padding-right: 1ex;
}

.artifact-filter {
width: 250px;
}

.artifact-search {
z-index: 1000;
}

.artifact-search-input {
height: 38px;
}

.form-inline .artifact-search-input.form-control {
width: 300px;
}

0 comments on commit e9aa032

Please sign in to comment.