Skip to content

Commit cf5daf3

Browse files
committed
- Removed messy writer implementation
- Fixed reader implementation - Fixed memory leaks
1 parent cab0dd9 commit cf5daf3

File tree

9 files changed

+99
-355
lines changed

9 files changed

+99
-355
lines changed

README.md

+55-43
Original file line numberDiff line numberDiff line change
@@ -1,87 +1,99 @@
1-
# Yara/ClamAV/Other Combo
1+
# Yara scanner for Golang
22

3-
Yara scanner library compatible with `io.TeeReader` for streaming
3+
Yara scanner library compatible with `io.Reader` for streaming
44

5-
## Requirements
6-
7-
All requirements from [github.com/hillu/go-yara](https://github.com/hillu/go-yara) apply
8-
9-
## Implementation
5+
## Features
106

11-
### Must implement WriteCloser
7+
- Read multiple rules for multiple directories
8+
- Scan inside archives with maximum depth
129

13-
When using a Tee reader, the `TeeReaderAutoClose` should be used to clean resources.
10+
## Requirements
1411

15-
Make sure to call `Close()` if you're doing a custom implementation.
12+
All requirements from [github.com/hillu/go-yara](https://github.com/hillu/go-yara) apply
1613

1714
## Example
1815

1916
```golang
2017
package main
2118

2219
import (
23-
"context"
2420
"crypto/sha1"
2521
"crypto/sha256"
26-
"encoding/hex"
2722
"flag"
2823
"github.com/LeakIX/YaraStream"
29-
"github.com/elvinchan/clamd"
3024
"io"
25+
"io/fs"
3126
"log"
3227
"os"
28+
"path/filepath"
29+
"runtime"
30+
"sync"
3331
)
3432

35-
var (
36-
clamdSock = flag.String("clamd-sock", "", "ClamD socket")
37-
)
33+
var yaraScanner *YaraStream.YaraScanner
34+
var fileChan = make(chan string)
35+
var wg sync.WaitGroup
3836

3937
func main() {
38+
var err error
4039
flag.Parse()
4140
if len(flag.Args()) != 1 {
4241
log.Fatal("You must provide a file to scan")
4342
}
44-
clamClient, err := clamd.NewClient("unix", *clamdSock)
45-
if err != nil {
46-
panic(err)
47-
}
48-
scanner, err := YaraStream.NewYaraScanner(
43+
yaraScanner, err = YaraStream.NewYaraScanner(
4944
YaraStream.RuleDirectory{Namespace: "AbuseCH", Path: "./rules/abusech"},
5045
YaraStream.RuleDirectory{Namespace: "ReversingLabs", Path: "./rules/reversinglabs"},
5146
YaraStream.RuleDirectory{Namespace: "ESET", Path: "./rules/eset"},
47+
YaraStream.RuleDirectory{Namespace: "AlienVault", Path: "./rules/otx"},
5248
)
5349
if err != nil {
5450
panic(err)
5551
}
56-
file, err := os.Open(flag.Arg(0))
52+
53+
for w := 1; w <= runtime.NumCPU(); w++ {
54+
go worker()
55+
}
56+
err = filepath.Walk(flag.Arg(0), func(path string, info fs.FileInfo, err error) error {
57+
if err != nil {
58+
return nil
59+
}
60+
if info.IsDir() || !info.Mode().IsRegular() {
61+
return nil
62+
}
63+
wg.Add(1)
64+
fileChan <- path
65+
return nil
66+
})
67+
5768
if err != nil {
5869
panic(err)
5970
}
60-
yaraWriter := scanner.NewYaraWriter()
71+
wg.Wait()
72+
}
73+
74+
func worker() {
75+
for filePath := range fileChan {
76+
scanFile(filePath)
77+
wg.Done()
78+
}
79+
}
80+
81+
func scanFile(path string) error {
82+
log.Printf("Scanning %s", path)
83+
file, err := os.Open(path)
84+
if err != nil {
85+
log.Println(err)
86+
return nil
87+
}
6188
sha256Hasher := sha256.New()
6289
sha1Hasher := sha1.New()
6390
sha256Tee := io.TeeReader(file, sha256Hasher)
6491
sha1Tee := io.TeeReader(sha256Tee, sha1Hasher)
65-
yaraTee := YaraStream.TeeReaderAutoClose(sha1Tee, yaraWriter)
66-
scanResults, err := clamClient.ScanReader(context.Background(), yaraTee)
67-
if err != nil {
68-
panic(err)
69-
}
70-
infected := false
71-
for _, scanResult := range scanResults {
72-
if scanResult.Status == "FOUND" {
73-
infected = true
74-
log.Printf("[ClamAV] Infection found: %s", scanResult.Signature)
75-
}
76-
}
77-
for _, scanResult := range yaraWriter.MatchedRules {
78-
infected = true
79-
log.Printf("[YARA] Infection found: %s/%s", scanResult.Namespace(), scanResult.Identifier())
80-
}
81-
log.Printf("SHA256: %s", hex.EncodeToString(sha256Hasher.Sum(nil)))
82-
log.Printf("SHA1: %s", hex.EncodeToString(sha1Hasher.Sum(nil)))
83-
if infected {
84-
os.Exit(1)
92+
matches, err := yaraScanner.ScanReader(sha1Tee, YaraStream.WithFilenameTip(path), YaraStream.WithMaxLevel(3))
93+
for _, scanResult := range matches {
94+
log.Printf("[YARA] Infection found: %s/%s in %s\n", scanResult.Namespace(), scanResult.Identifier(), path)
8595
}
96+
return nil
8697
}
98+
8799
```

TeeAutoCloser.go

-30
This file was deleted.

YaraReader.go

+30-11
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,20 @@ type YaraReader struct {
2020
Infected bool
2121
filename string
2222
level int
23+
maxBlockSize int
2324
}
2425

26+
// ScanReader Will scan a given reader until EOF or an error happens. It will scan archives.
2527
func (s *YaraScanner) ScanReader(reader io.Reader, opts ...func(writer *YaraReader)) ([]*yara.Rule, error) {
26-
bufferedReader := bufio.NewReaderSize(reader, 16*1024)
2728
testReader := &YaraReader{
28-
r: bufferedReader,
29-
level: 10,
29+
level: 10,
30+
maxBlockSize: 16 * 1024,
3031
}
3132
for _, option := range opts {
3233
option(testReader)
3334
}
34-
dec, err := decoder.GetDecoder("", testReader.r)
35+
testReader.r = bufio.NewReaderSize(reader, testReader.maxBlockSize)
36+
dec, err := decoder.GetDecoder(testReader.filename, testReader.r)
3537
if err == nil {
3638
if testReader.level < 1 {
3739
return nil, nil
@@ -42,7 +44,11 @@ func (s *YaraScanner) ScanReader(reader io.Reader, opts ...func(writer *YaraRead
4244
return testReader.mrs, nil
4345
}
4446
if entry.IsFile() {
45-
partResults, _ := s.ScanReader(dec, ReaderWithFilenameTip(entry.Filename), ReaderWithCurrentLevel(testReader.level-1))
47+
partResults, _ := s.ScanReader(dec,
48+
WithFilenameTip(entry.Filename),
49+
WithMaxLevel(testReader.level-1),
50+
WithBlockSize(testReader.maxBlockSize),
51+
)
4652
testReader.mrs = append(testReader.mrs, partResults...)
4753
}
4854
}
@@ -51,13 +57,14 @@ func (s *YaraScanner) ScanReader(reader io.Reader, opts ...func(writer *YaraRead
5157
defer testReader.scanner.Destroy()
5258
testReader.scanner.SetFlags(yara.ScanFlagsProcessMemory)
5359
testReader.scanner.SetCallback(testReader)
54-
testReader.buf = make([]byte, 16*1024)
55-
testReader.firstBlockData = make([]byte, 16*1024)
60+
testReader.buf = make([]byte, testReader.maxBlockSize)
61+
testReader.firstBlockData = make([]byte, testReader.maxBlockSize)
5662
err = testReader.scanner.ScanMemBlocks(testReader)
5763

5864
return testReader.mrs, err
5965
}
6066

67+
// First Will fetch the first block and cache it in our reader for further calls
6168
func (s *YaraReader) First() *yara.MemoryBlock {
6269
if s.firstBlock == nil {
6370
s.firstBlock = s.Next()
@@ -71,6 +78,7 @@ func (s *YaraReader) First() *yara.MemoryBlock {
7178
return s.firstBlock
7279
}
7380

81+
// Next Will fetch the next block for scanning
7482
func (s *YaraReader) Next() *yara.MemoryBlock {
7583
n, err := s.r.Read(s.buf)
7684
if err != nil && n == 0 {
@@ -85,29 +93,40 @@ func (s *YaraReader) Next() *yara.MemoryBlock {
8593
}
8694
}
8795

96+
// RuleMatching will be called by the engine when a rule is matched
8897
func (y *YaraReader) RuleMatching(_ *yara.ScanContext, rule *yara.Rule) (bool, error) {
8998
y.Infected = true
9099
y.mrs = append(y.mrs, rule)
91100
return true, nil
92101
}
93102

103+
// copy Helper for Next()
94104
func (s *YaraReader) copy(buf []byte) {
95105
copy(buf, s.buf[:s.length])
96106
}
107+
108+
// first Helper for First()
97109
func (s *YaraReader) first(buf []byte) {
98110
copy(buf, s.firstBlockData[:s.firstBlock.Size])
99-
//log.Println(s.filename, s.firstBlock.Base, s.firstBlock.Size, string(buf))
100-
101111
}
102112

103-
func ReaderWithFilenameTip(filename string) func(writer *YaraReader) {
113+
// WithFilenameTip Will tip the Decoder on possible archive types
114+
func WithFilenameTip(filename string) func(writer *YaraReader) {
104115
return func(writer *YaraReader) {
105116
writer.filename = filename
106117
}
107118
}
108119

109-
func ReaderWithCurrentLevel(level int) func(writer *YaraReader) {
120+
// WithMaxLevel Will prevent the Reader to inspect archives under and given level
121+
func WithMaxLevel(level int) func(writer *YaraReader) {
110122
return func(writer *YaraReader) {
111123
writer.level = level
112124
}
113125
}
126+
127+
// WithBlockSize Sets the default buffer and block size for in-memory scanning
128+
func WithBlockSize(size int) func(writer *YaraReader) {
129+
return func(writer *YaraReader) {
130+
writer.maxBlockSize = size
131+
}
132+
}

YaraScanner.go

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type RuleDirectory struct {
1717
Path string
1818
}
1919

20+
// NewYaraScanner Will return a new scanner with rules compiled
2021
func NewYaraScanner(ruleDirectories ...RuleDirectory) (*YaraScanner, error) {
2122
scanner := &YaraScanner{}
2223
compiler, err := yara.NewCompiler()

0 commit comments

Comments
 (0)