yarascanner/main.go

315 lines
5.9 KiB
Go
Raw Normal View History

2021-02-25 02:46:02 +00:00
package main
import (
"encoding/json"
"github.com/go-chi/chi"
2021-02-25 03:00:39 +00:00
"github.com/hillu/go-yara/v4"
"github.com/package-url/packageurl-go"
2021-02-25 02:46:02 +00:00
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
"io"
"net/http"
"os"
"os/signal"
"runtime"
"strings"
"sync"
"sync/atomic"
2021-02-25 02:46:02 +00:00
"syscall"
"time"
)
type callbackFunc func(yara.MatchRules, error)
2021-02-25 02:46:02 +00:00
type Job struct {
Data io.ReadCloser
Callback callbackFunc
2021-02-25 02:46:02 +00:00
}
var (
client *http.Client
jobChan = make(chan *Job)
jobCount int64
detectedCount int64
2021-02-25 02:46:02 +00:00
)
type statsResponse struct {
Processed int64 `json:"jobsProcessed"`
Detected int64 `json:"detectionCount""`
}
type response struct {
Success bool `json:"success"`
2021-03-08 02:27:47 +00:00
Error error `json:"error,omitempty"`
MatchedRules yara.MatchRules `json:"rules,omitempty"`
}
type multiResponse struct {
Success bool `json:"success"`
Files map[string]yara.MatchRules `json:"files,omitempty"`
}
2021-02-25 02:46:02 +00:00
func main() {
viper.SetDefault("threads", runtime.NumCPU())
viper.SetDefault("rules", "pkg:github/Neo23x0/signature-base#yara")
2021-03-07 05:03:48 +00:00
viper.SetDefault("bind", ":8080")
2021-02-25 02:46:02 +00:00
viper.AutomaticEnv()
client = &http.Client{
Timeout: 15 * time.Second,
}
c, err := yara.NewCompiler()
if err != nil {
log.WithError(err).Fatal("Unable to setup new compiler")
}
2021-03-07 04:48:25 +00:00
c.DefineVariable("filename", "")
2021-03-07 04:47:15 +00:00
c.DefineVariable("filepath", "")
2021-03-07 04:48:25 +00:00
c.DefineVariable("extension", "")
c.DefineVariable("filetype", "")
2021-03-07 04:47:15 +00:00
2021-03-07 04:52:50 +00:00
log.Info("Loading rules")
2021-02-25 02:46:02 +00:00
loadRules(c)
rules, err := c.GetRules()
if err != nil {
log.WithError(err).Fatal("Unable to compile rules")
}
threads := viper.GetInt("threads")
log.WithField("workers", threads).Info("Starting workers")
for i := 0; i < threads; i++ {
go worker(rules)
2021-02-25 02:46:02 +00:00
}
r := chi.NewRouter()
2021-02-25 02:46:02 +00:00
r.Get("/stats", statsHandler)
r.Post("/scan", scanHandler)
2021-02-25 02:46:02 +00:00
2021-03-07 05:03:48 +00:00
bind := viper.GetString("bind")
log.WithField("bind", bind).Info("Binding to address")
go http.ListenAndServe(bind, r)
2021-02-25 02:46:02 +00:00
ch := make(chan os.Signal)
signal.Notify(ch, syscall.SIGKILL, syscall.SIGTERM, syscall.SIGINT)
2021-02-25 03:00:39 +00:00
<-ch
2021-02-25 02:46:02 +00:00
}
func statsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(statsResponse{Processed: atomic.LoadInt64(&jobCount), Detected: atomic.LoadInt64(&detectedCount)})
}
// HTTP handler for scanning files
func scanHandler(w http.ResponseWriter, r *http.Request) {
contentType := r.Header.Get("Content-Type")
2021-02-25 02:46:02 +00:00
if idx := strings.Index(contentType, ";"); idx != -1 {
contentType = contentType[0:idx]
}
2021-02-25 02:46:02 +00:00
2021-03-08 02:48:22 +00:00
var res interface{}
wg := &sync.WaitGroup{}
switch contentType {
case "multipart/form-data":
if r.MultipartForm == nil {
r.ParseMultipartForm(32 << 20)
2021-02-25 02:46:02 +00:00
}
log.WithField("contentType", contentType).Debug("Adding files from multipart form as jobs")
2021-03-07 05:18:28 +00:00
fileCount := 0
2021-03-07 05:18:28 +00:00
2021-03-08 02:48:22 +00:00
response := multiResponse{
Success: true,
Files: make(map[string]yara.MatchRules),
2021-02-25 02:46:02 +00:00
}
fileLock := &sync.Mutex{}
2021-03-07 05:02:55 +00:00
for _, files := range r.MultipartForm.File {
// Append files
for _, file := range files {
wg.Add(1)
2021-02-25 02:46:02 +00:00
2021-03-07 05:02:55 +00:00
fileCount++
f, err := file.Open()
2021-02-25 02:46:02 +00:00
if err != nil {
continue
}
2021-02-25 02:46:02 +00:00
job := &Job{
Data: f,
Callback: func(m yara.MatchRules, err error) {
fileLock.Lock()
2021-03-08 02:48:22 +00:00
response.Files[file.Filename] = m
fileLock.Unlock()
wg.Done()
},
}
2021-02-25 02:46:02 +00:00
jobChan <- job
}
2021-02-25 02:46:02 +00:00
}
2021-03-07 05:02:55 +00:00
log.WithField("count", fileCount).Debug("Waiting for jobs to finish")
2021-03-08 02:48:22 +00:00
wg.Wait()
log.WithField("matches", res).Debug("Matched rules")
2021-03-08 02:44:10 +00:00
2021-03-08 02:48:22 +00:00
res = response
default:
2021-03-08 02:48:22 +00:00
wg.Add(1)
job := &Job{
Data: r.Body,
Callback: func(m yara.MatchRules, err error) {
2021-03-07 05:18:28 +00:00
log.WithField("match", m).Debug("Matched rules")
if err != nil {
2021-03-08 02:48:22 +00:00
res = response{Success: false, Error: err}
2021-03-07 05:18:28 +00:00
} else {
2021-03-08 02:48:22 +00:00
res = response{Success: true, MatchedRules: m}
2021-03-08 02:44:10 +00:00
}
if err != nil {
log.WithError(err).Error("Unable to send response")
2021-03-07 05:18:28 +00:00
}
2021-03-08 02:48:22 +00:00
wg.Done()
},
}
2021-02-25 02:46:02 +00:00
2021-03-07 05:02:55 +00:00
log.WithField("contentType", contentType).Debug("Scanning contents of body")
jobChan <- job
2021-02-25 02:46:02 +00:00
}
2021-03-08 02:48:22 +00:00
wg.Wait()
w.Header().Set("Content-Type", "application/json")
2021-03-08 02:48:22 +00:00
err := json.NewEncoder(w).Encode(res)
if err != nil {
log.WithError(err).Error("Unable to send response")
}
}
2021-02-25 02:46:02 +00:00
// Load rules from the rules configuration option
func loadRules(c *yara.Compiler) {
rulePaths := strings.Split(viper.GetString("rules"), ",")
2021-02-25 02:46:02 +00:00
for _, p := range rulePaths {
2021-03-07 04:52:50 +00:00
log.WithField("package", p).Info("Loading rules from package")
instance, err := packageurl.FromString(p)
2021-02-25 02:46:02 +00:00
if err != nil {
log.WithFields(log.Fields{
"error": err,
"package": p,
}).Fatalln("Invalid rule URL")
}
2021-02-25 02:46:02 +00:00
switch instance.Type {
case "git", "bitbucket", "github", "gitlab":
err = loadRulesFromGit(instance, c)
case "http", "https":
err = loadRulesFromHttp(instance, c)
}
2021-02-25 02:46:02 +00:00
if err != nil {
log.WithFields(log.Fields{
"error": err,
"package": p,
}).Fatalln("Unable to load rules")
}
2021-02-25 02:46:02 +00:00
}
}
2021-02-25 02:46:02 +00:00
// Load rules from http(s)
// TODO: Support archive files alongside standard yar files
func loadRulesFromHttp(pkg packageurl.PackageURL, c *yara.Compiler) error {
2021-03-09 04:18:19 +00:00
res, err := client.Get(pkg.Type + "://" + pkg.Namespace + "/" + pkg.Name)
2021-02-25 02:46:02 +00:00
if err != nil {
return err
2021-02-25 02:46:02 +00:00
}
defer res.Body.Close()
2021-02-25 02:46:02 +00:00
b, err := io.ReadAll(res.Body)
2021-02-25 02:46:02 +00:00
if err != nil {
return err
2021-02-25 02:46:02 +00:00
}
return c.AddString(string(b), "")
2021-02-25 02:46:02 +00:00
}
// A worker routine. Creates a new scanner instance and pulls jobs.
func worker(rules *yara.Rules) {
2021-02-25 02:46:02 +00:00
s, err := yara.NewScanner(rules)
if err != nil {
panic(err)
}
for {
job := <-jobChan
2021-02-25 02:46:02 +00:00
2021-03-07 05:02:55 +00:00
log.Debug("Processing job")
2021-02-25 02:46:02 +00:00
processJob(s, job)
}
}
func processJob(s *yara.Scanner, job *Job) {
m := make(yara.MatchRules, 0)
2021-02-25 03:00:39 +00:00
atomic.AddInt64(&jobCount, 1)
defer job.Data.Close()
2021-02-25 02:46:02 +00:00
b, err := io.ReadAll(job.Data)
2021-02-25 02:46:02 +00:00
if err != nil {
2021-03-07 05:18:28 +00:00
job.Callback(nil, err)
2021-02-25 02:46:02 +00:00
return
}
err = s.SetCallback(&m).ScanMem(b)
2021-02-25 02:46:02 +00:00
if err != nil {
2021-03-07 05:18:28 +00:00
job.Callback(nil, err)
2021-02-25 02:46:02 +00:00
return
}
if len(m) > 0 {
atomic.AddInt64(&detectedCount, 1)
}
job.Callback(m, nil)
2021-02-25 03:00:39 +00:00
}