231 lines
6.5 KiB
Go
231 lines
6.5 KiB
Go
// Licensed to Elasticsearch B.V. under one or more contributor
|
|
// license agreements. See the NOTICE file distributed with
|
|
// this work for additional information regarding copyright
|
|
// ownership. Elasticsearch B.V. licenses this file to you under
|
|
// the Apache License, Version 2.0 (the "License"); you may
|
|
// not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing,
|
|
// software distributed under the License is distributed on an
|
|
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
// KIND, either express or implied. See the License for the
|
|
// specific language governing permissions and limitations
|
|
// under the License.
|
|
|
|
package wineventlog
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/joeshaw/multierror"
|
|
)
|
|
|
|
const (
|
|
query = `<QueryList>
|
|
<Query Id="0">
|
|
<Select Path="{{.Path}}">*{{if .Select}}[System[{{join .Select " and "}}]]{{end}}</Select>{{if .Suppress}}
|
|
<Suppress Path="{{.Path}}">*[System[({{join .Suppress " or "}})]]</Suppress>{{end}}
|
|
</Query>
|
|
</QueryList>`
|
|
)
|
|
|
|
var (
|
|
templateFuncMap = template.FuncMap{"join": strings.Join}
|
|
queryTemplate = template.Must(template.New("query").Funcs(templateFuncMap).Parse(query))
|
|
incEventIDRegex = regexp.MustCompile(`^\d+$`)
|
|
incEventIDRangeRegex = regexp.MustCompile(`^(\d+)\s*-\s*(\d+)$`)
|
|
excEventIDRegex = regexp.MustCompile(`^-(\d+)$`)
|
|
)
|
|
|
|
// Query that identifies the source of the events and one or more selectors or
|
|
// suppressors.
|
|
type Query struct {
|
|
// Name of the channel or the path to the log file that contains the events
|
|
// to query.
|
|
Log string
|
|
|
|
IgnoreOlder time.Duration // Ignore records older than this time period.
|
|
|
|
// Whitelist and blacklist of event IDs. The value is a comma-separated
|
|
// list. The accepted values are single event IDs to include (e.g. 4634), a
|
|
// range of event IDs to include (e.g. 4400-4500), and single event IDs to
|
|
// exclude (e.g. -4410).
|
|
EventID string
|
|
|
|
// Level or levels to include. The value is a comma-separated list of levels
|
|
// to include. The accepted levels are verbose (5), information (4),
|
|
// warning (3), error (2), and critical (1).
|
|
Level string
|
|
|
|
// Providers (sources) to include records from.
|
|
Provider []string
|
|
}
|
|
|
|
// Build builds a query from the given parameters. The query is returned as a
|
|
// XML string and can be used with Subscribe function.
|
|
func (q Query) Build() (string, error) {
|
|
var errs multierror.Errors
|
|
if q.Log == "" {
|
|
errs = append(errs, fmt.Errorf("empty log name"))
|
|
}
|
|
|
|
qp := &queryParams{Path: q.Log}
|
|
builders := []func(Query) error{
|
|
qp.ignoreOlderSelect,
|
|
qp.eventIDSelect,
|
|
qp.levelSelect,
|
|
qp.providerSelect,
|
|
}
|
|
for _, build := range builders {
|
|
if err := build(q); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
if len(errs) > 0 {
|
|
return "", errs.Err()
|
|
}
|
|
return executeTemplate(queryTemplate, qp)
|
|
}
|
|
|
|
// queryParams are the parameters that are used to create a query from a
|
|
// template.
|
|
type queryParams struct {
|
|
Path string
|
|
Select []string
|
|
Suppress []string
|
|
}
|
|
|
|
func (qp *queryParams) ignoreOlderSelect(q Query) error {
|
|
if q.IgnoreOlder <= 0 {
|
|
return nil
|
|
}
|
|
|
|
ms := q.IgnoreOlder.Nanoseconds() / int64(time.Millisecond)
|
|
qp.Select = append(qp.Select,
|
|
fmt.Sprintf("TimeCreated[timediff(@SystemTime) <= %d]", ms))
|
|
return nil
|
|
}
|
|
|
|
func (qp *queryParams) eventIDSelect(q Query) error {
|
|
if q.EventID == "" {
|
|
return nil
|
|
}
|
|
|
|
var includes []string
|
|
var excludes []string
|
|
components := strings.Split(q.EventID, ",")
|
|
for _, c := range components {
|
|
c = strings.TrimSpace(c)
|
|
switch {
|
|
case incEventIDRegex.MatchString(c):
|
|
includes = append(includes, fmt.Sprintf("EventID=%s", c))
|
|
case excEventIDRegex.MatchString(c):
|
|
m := excEventIDRegex.FindStringSubmatch(c)
|
|
excludes = append(excludes, fmt.Sprintf("EventID=%s", m[1]))
|
|
case incEventIDRangeRegex.MatchString(c):
|
|
m := incEventIDRangeRegex.FindStringSubmatch(c)
|
|
r1, _ := strconv.Atoi(m[1])
|
|
r2, _ := strconv.Atoi(m[2])
|
|
if r1 >= r2 {
|
|
return fmt.Errorf("event ID range '%s' is invalid", c)
|
|
}
|
|
includes = append(includes,
|
|
fmt.Sprintf("(EventID >= %d and EventID <= %d)", r1, r2))
|
|
default:
|
|
return fmt.Errorf("invalid event ID query component ('%s')", c)
|
|
}
|
|
}
|
|
|
|
if len(includes) == 1 {
|
|
qp.Select = append(qp.Select, includes...)
|
|
} else if len(includes) > 1 {
|
|
qp.Select = append(qp.Select, "("+strings.Join(includes, " or ")+")")
|
|
}
|
|
if len(excludes) == 1 {
|
|
qp.Suppress = append(qp.Suppress, excludes...)
|
|
} else if len(excludes) > 1 {
|
|
qp.Suppress = append(qp.Suppress, "("+strings.Join(excludes, " or ")+")")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// levelSelect returns a xpath selector for the event Level. The returned
|
|
// selector will select events with levels less than or equal to the specified
|
|
// level. Note that level 0 is used as a catch-all/unknown level.
|
|
//
|
|
// Accepted levels:
|
|
// verbose - 5
|
|
// information, info - 4 or 0
|
|
// warning, warn - 3
|
|
// error, err - 2
|
|
// critical, crit - 1
|
|
func (qp *queryParams) levelSelect(q Query) error {
|
|
if q.Level == "" {
|
|
return nil
|
|
}
|
|
|
|
l := func(level int) string { return fmt.Sprintf("Level = %d", level) }
|
|
|
|
var levelSelect []string
|
|
for _, expr := range strings.Split(q.Level, ",") {
|
|
expr = strings.TrimSpace(expr)
|
|
switch strings.ToLower(expr) {
|
|
default:
|
|
return fmt.Errorf("invalid level ('%s') for query", q.Level)
|
|
case "verbose", "5":
|
|
levelSelect = append(levelSelect, l(5))
|
|
case "information", "info", "4":
|
|
levelSelect = append(levelSelect, l(0), l(4))
|
|
case "warning", "warn", "3":
|
|
levelSelect = append(levelSelect, l(3))
|
|
case "error", "err", "2":
|
|
levelSelect = append(levelSelect, l(2))
|
|
case "critical", "crit", "1":
|
|
levelSelect = append(levelSelect, l(1))
|
|
case "0":
|
|
levelSelect = append(levelSelect, l(0))
|
|
}
|
|
}
|
|
|
|
if len(levelSelect) > 0 {
|
|
qp.Select = append(qp.Select, "("+strings.Join(levelSelect, " or ")+")")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (qp *queryParams) providerSelect(q Query) error {
|
|
if len(q.Provider) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var selects []string
|
|
for _, p := range q.Provider {
|
|
selects = append(selects, fmt.Sprintf("@Name='%s'", p))
|
|
}
|
|
|
|
qp.Select = append(qp.Select,
|
|
fmt.Sprintf("Provider[%s]", strings.Join(selects, " or ")))
|
|
return nil
|
|
}
|
|
|
|
// executeTemplate populates a template with the given data and returns the
|
|
// value as a string.
|
|
func executeTemplate(t *template.Template, data interface{}) (string, error) {
|
|
var buf bytes.Buffer
|
|
err := t.Execute(&buf, data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return buf.String(), nil
|
|
}
|