summaryrefslogtreecommitdiff
path: root/main.go
diff options
context:
space:
mode:
authorMathieu Deous2022-05-02 20:18:23 +0200
committerGitHub2022-05-02 20:18:23 +0200
commit48936efa96ae17295be4e0a71be3294f0ec6aef8 (patch)
treef4e69551f1368aa048edf46b7b061600f3668329 /main.go
parentbbc738e16f8b637afde58d65196374af98a5e0e2 (diff)
Make application go-install-able and create a docker image
Diffstat (limited to '')
-rw-r--r--main.go (renamed from php-malware-finder/phpmalwarefinder.go)210
1 files changed, 134 insertions, 76 deletions
diff --git a/php-malware-finder/phpmalwarefinder.go b/main.go
index 2a641b3..d5f8d69 100644
--- a/php-malware-finder/phpmalwarefinder.go
+++ b/main.go
@@ -2,8 +2,10 @@ package main
2 2
3import ( 3import (
4 "bytes" 4 "bytes"
5 "embed"
5 "fmt" 6 "fmt"
6 "io" 7 "io"
8 "io/fs"
7 "io/ioutil" 9 "io/ioutil"
8 "log" 10 "log"
9 "net/http" 11 "net/http"
@@ -17,89 +19,135 @@ import (
17 "github.com/jessevdk/go-flags" 19 "github.com/jessevdk/go-flags"
18) 20)
19 21
20const RulesURI = "https://raw.githubusercontent.com/jvoisin/php-malware-finder/master/php-malware-finder/" 22const (
21const RulesFile = "php.yar" 23 RulesURI = "https://raw.githubusercontent.com/jvoisin/php-malware-finder/master/php-malware-finder/data"
22const DefaultDir = "/etc/phpmalwarefinder" 24 RulesFile = "php.yar"
23const ScanMaxDuration = time.Duration(60) 25 ScanMaxDuration = time.Duration(60)
24const TooShort = "TooShort" 26 TooShort = "TooShort"
25const TooShortMaxLines = 2 27 TooShortMaxLines = 2
26const TooShortMinChars = 300 28 TooShortMinChars = 300
27const DangerousMatchWeight = 2 29 DangerousMatchWeight = 2
28const DangerousMinScore = 3 30 DangerousMinScore = 3
29const FileBufferSize = 32 * 1024 // 32KB 31 FileBufferSize = 32 * 1024 // 32KB
30const YaraMaxThreads = 32 32 YaraMaxThreads = 32
33 TempDirPrefix = "pmf-"
34)
31 35
32var args struct { // command-line arguments specs using github.com/jessevdk/go-flags 36var (
33 RulesDir string `short:"r" long:"rules-dir" description:"Rules location (default: /etc/phpmalwarefinder or .)"` 37 args struct { // command-line arguments specs using github.com/jessevdk/go-flags
34 ShowAll bool `short:"a" long:"show-all" description:"Display all matched rules"` 38 RulesDir string `short:"r" long:"rules-dir" description:"Alternative rules location (default: embedded rules)"`
35 Fast bool `short:"f" long:"fast" description:"Enable YARA's fast mode"` 39 ShowAll bool `short:"a" long:"show-all" description:"Display all matched rules"`
36 RateLimit int `short:"R" long:"rate-limit" description:"Max. filesystem ops per second, 0 for no limit" default:"0"` 40 Fast bool `short:"f" long:"fast" description:"Enable YARA's fast mode"`
37 Verbose bool `short:"v" long:"verbose" description:"Verbose mode"` 41 RateLimit int `short:"R" long:"rate-limit" description:"Max. filesystem ops per second, 0 for no limit" default:"0"`
38 Workers int `short:"w" long:"workers" description:"Number of workers to spawn for scanning" default:"32"` 42 Verbose bool `short:"v" long:"verbose" description:"Verbose mode"`
39 LongLines bool `short:"L" long:"long-lines" description:"Check long lines"` 43 Workers int `short:"w" long:"workers" description:"Number of workers to spawn for scanning" default:"32"`
40 ExcludeCommon bool `short:"c" long:"exclude-common" description:"Do not scan files with common extensions"` 44 LongLines bool `short:"L" long:"long-lines" description:"Check long lines"`
41 ExcludeImgs bool `short:"i" long:"exclude-imgs" description:"Do not scan image files"` 45 ExcludeCommon bool `short:"c" long:"exclude-common" description:"Do not scan files with common extensions"`
42 ExcludedExts []string `short:"x" long:"exclude-ext" description:"Additional file extensions to exclude"` 46 ExcludeImgs bool `short:"i" long:"exclude-imgs" description:"Do not scan image files"`
43 Update bool `short:"u" long:"update" description:"Update rules"` 47 ExcludedExts []string `short:"x" long:"exclude-ext" description:"Additional file extensions to exclude"`
44 Positional struct { 48 Update bool `short:"u" long:"update" description:"Update rules"`
45 Target string 49 ShowVersion bool `short:"V" long:"version" description:"Show version number and exit"`
46 } `positional-args:"yes"` 50 Positional struct {
47} 51 Target string
48var scanFlags yara.ScanFlags 52 } `positional-args:"yes"`
49var stoppedWorkers int 53 }
50var lineFeed = []byte{'\n'} 54 scanFlags yara.ScanFlags
51var dangerousMatches = map[string]struct{}{ 55 stoppedWorkers int
52 "PasswordProtection": {}, 56 lineFeed = []byte{'\n'}
53 "Websites": {}, 57 dangerousMatches = map[string]struct{}{
54 "TooShort": {}, 58 "PasswordProtection": {},
55 "NonPrintableChars": {}, 59 "Websites": {},
56} 60 "TooShort": {},
57var excludedDirs = [...]string{ 61 "NonPrintableChars": {},
58 "/.git/", "/.hg/", "/.svn/", "/.CVS/", 62 }
59} 63 excludedDirs = [...]string{
60var excludedExts = map[string]struct{}{} 64 "/.git/", "/.hg/", "/.svn/", "/.CVS/",
61var commonExts = [...]string{ 65 }
62 ".js", ".coffee", ".map", ".min", ".css", ".less", // static files 66 excludedExts = map[string]struct{}{}
63 ".zip", ".rar", ".7z", ".gz", ".bz2", ".xz", ".tar", ".tgz", // archives 67 commonExts = [...]string{
64 ".txt", ".csv", ".json", ".rst", ".md", ".yaml", ".yml", // plain text 68 ".js", ".coffee", ".map", ".min", ".css", ".less", // static files
65 ".so", ".dll", ".bin", ".exe", ".bundle", // binaries 69 ".zip", ".rar", ".7z", ".gz", ".bz2", ".xz", ".tar", ".tgz", // archives
66} 70 ".txt", ".csv", ".json", ".rst", ".md", ".yaml", ".yml", // plain text
67var imageExts = [...]string{ 71 ".so", ".dll", ".bin", ".exe", ".bundle", // binaries
68 ".png", ".jpg", ".jpeg", ".gif", ".svg", ".bmp", ".ico", 72 }
69} 73 imageExts = [...]string{
70var scannedFilesCount int 74 ".png", ".jpg", ".jpeg", ".gif", ".svg", ".bmp", ".ico",
75 }
76 scannedFilesCount int
77 rulesFiles = [...]string{
78 RulesFile, "whitelist.yar",
79 "whitelists/custom.yar", "whitelists/drupal.yar", "whitelists/magento1ce.yar", "whitelists/magento2.yar",
80 "whitelists/phpmyadmin.yar", "whitelists/prestashop.yar", "whitelists/symfony.yar", "whitelists/wordpress.yar",
81 }
82 tempDirPathPrefix = path.Join(os.TempDir(), TempDirPrefix)
83 version = "dev"
84
85 //go:embed data/php.yar data/whitelist.yar data/whitelists
86 data embed.FS
87)
71 88
72// handleError is a generic error handler which displays the error message to the user and exits if required. 89// handleError is a generic error handler which displays an error message to the user and exits if required.
73func handleError(err error, exit bool) { 90func handleError(err error, desc string, isFatal bool) {
74 if err != nil { 91 if err != nil {
75 log.Println("[ERROR]", err) 92 if desc != "" {
76 if exit { 93 desc = " " + desc + ":"
94 }
95 log.Println("[ERROR]"+desc, err)
96 if isFatal {
77 os.Exit(1) 97 os.Exit(1)
78 } 98 }
79 } 99 }
80} 100}
81 101
102// writeRulesFiles copies the rules from the content of a `fs.FS` to a temporary folder and
103// returns its location.
104func writeRulesFiles(content fs.FS) string {
105 // create temporary folder structure
106 rulesPath, err := ioutil.TempDir(os.TempDir(), TempDirPrefix)
107 handleError(err, "unable to create temporary folder", true)
108 err = os.Mkdir(path.Join(rulesPath, "whitelists"), 0755)
109 handleError(err, "unable to create temporary subfolder", true)
110
111 // write each YARA file to the disk
112 for _, rulesFile := range rulesFiles {
113 // read embedded content
114 f, err := content.Open(path.Join("data", rulesFile))
115 handleError(err, "unable to read embedded rule", true)
116 ruleData, err := ioutil.ReadAll(f)
117
118 // write to temporary file
119 err = os.WriteFile(path.Join(rulesPath, rulesFile), ruleData, 0640)
120 handleError(err, "unable to write rule to disk", true)
121 err = f.Close()
122 handleError(err, "unable to close rules file", false)
123 }
124 return rulesPath
125}
126
82// updateRules downloads latest YARA rules from phpmalwarefinder GitHub repository. 127// updateRules downloads latest YARA rules from phpmalwarefinder GitHub repository.
83// Download location is either `args.RulesDir`, `/etc/phpmalwarefinder`, or the current directory. 128// Download location is either `args.RulesDir`, `/etc/phpmalwarefinder`, or the current directory.
84func updateRules() { 129func updateRules() {
130 if strings.HasPrefix(args.RulesDir, tempDirPathPrefix) {
131 handleError(fmt.Errorf("rules folder must be specified to update"), "", true)
132 }
85 if args.Verbose { 133 if args.Verbose {
86 log.Println("[DEBUG] updating ruleset") 134 log.Println("[DEBUG] updating ruleset")
87 } 135 }
88 136
89 downloadFile := func(uri string) []byte { 137 downloadFile := func(uri string) []byte {
90 resp, err := http.Get(uri) 138 resp, err := http.Get(uri)
91 handleError(err, true) 139 handleError(err, "unable to download rule", true)
92 defer func() { 140 defer func() {
93 err := resp.Body.Close() 141 err := resp.Body.Close()
94 handleError(err, false) 142 handleError(err, "unable to close response body", false)
95 }() 143 }()
96 data, err := ioutil.ReadAll(resp.Body) 144 data, err := ioutil.ReadAll(resp.Body)
97 handleError(err, true) 145 handleError(err, "unable to read response body", false)
98 return data 146 return data
99 } 147 }
100 writeFile := func(dst string, data []byte) { 148 writeFile := func(dst string, data []byte) {
101 err := ioutil.WriteFile(dst, data, 0440) 149 err := ioutil.WriteFile(dst, data, 0640)
102 handleError(err, true) 150 handleError(err, "unable to write downloaded file", true)
103 } 151 }
104 152
105 rulesFiles := [...]string{ 153 rulesFiles := [...]string{
@@ -122,10 +170,12 @@ func updateRules() {
122// File reading is done using a 32KB buffer to minimize memory usage. 170// File reading is done using a 32KB buffer to minimize memory usage.
123func fileStats(filepath string) (int, int, error) { 171func fileStats(filepath string) (int, int, error) {
124 f, err := os.Open(filepath) 172 f, err := os.Open(filepath)
125 handleError(err, true) 173 if err != nil {
174 return 0, 0, err
175 }
126 defer func() { 176 defer func() {
127 err := f.Close() 177 err := f.Close()
128 handleError(err, false) 178 handleError(err, "unable to close file", false)
129 }() 179 }()
130 charCount, lineCount := 0, 0 180 charCount, lineCount := 0, 0
131 buf := make([]byte, FileBufferSize) 181 buf := make([]byte, FileBufferSize)
@@ -145,7 +195,7 @@ func fileStats(filepath string) (int, int, error) {
145// makeScanner creates a YARA scanner with the appropriate options set. 195// makeScanner creates a YARA scanner with the appropriate options set.
146func makeScanner(rules *yara.Rules) *yara.Scanner { 196func makeScanner(rules *yara.Rules) *yara.Scanner {
147 scanner, err := yara.NewScanner(rules) 197 scanner, err := yara.NewScanner(rules)
148 handleError(err, true) 198 handleError(err, "unable to create YARA scanner", true)
149 scanner.SetFlags(scanFlags) 199 scanner.SetFlags(scanFlags)
150 scanner.SetTimeout(ScanMaxDuration) 200 scanner.SetTimeout(ScanMaxDuration)
151 return scanner 201 return scanner
@@ -163,7 +213,7 @@ func processFiles(rules *yara.Rules, targets <-chan string, results chan<- map[s
163 213
164 if args.LongLines { 214 if args.LongLines {
165 charCount, lineCount, err := fileStats(target) 215 charCount, lineCount, err := fileStats(target)
166 handleError(err, false) 216 handleError(err, "unable to get file stats", false)
167 if lineCount <= TooShortMaxLines && charCount >= TooShortMinChars { 217 if lineCount <= TooShortMaxLines && charCount >= TooShortMinChars {
168 tooShort := yara.MatchRule{Rule: TooShort} 218 tooShort := yara.MatchRule{Rule: TooShort}
169 result[target] = append(result[target], tooShort) 219 result[target] = append(result[target], tooShort)
@@ -206,7 +256,7 @@ func scanDir(dirName string, targets chan<- string, ticker <-chan time.Time) {
206 return nil 256 return nil
207 } 257 }
208 err := filepath.Walk(dirName, visit) 258 err := filepath.Walk(dirName, visit)
209 handleError(err, false) 259 handleError(err, "unable to complete target crawling", false)
210 close(targets) 260 close(targets)
211} 261}
212 262
@@ -249,18 +299,17 @@ func loadRulesFile(fileName string) (*yara.Rules, error) {
249func main() { 299func main() {
250 startTime := time.Now() 300 startTime := time.Now()
251 _, err := flags.Parse(&args) 301 _, err := flags.Parse(&args)
252 handleError(err, true) 302 if err != nil {
303 os.Exit(1)
304 }
305 if args.ShowVersion {
306 println(version)
307 os.Exit(0)
308 }
253 309
254 // check rules path 310 // check rules path
255 if args.RulesDir == "" { 311 if args.RulesDir == "" {
256 args.RulesDir = DefaultDir 312 args.RulesDir = writeRulesFiles(data)
257 if _, err := os.Stat(args.RulesDir); os.IsNotExist(err) {
258 args.RulesDir, _ = os.Getwd()
259 sigFile := path.Join(args.RulesDir, RulesFile)
260 if _, err = os.Stat(sigFile); os.IsNotExist(err) {
261 handleError(fmt.Errorf("no rules in %s or %s", DefaultDir, args.RulesDir), true)
262 }
263 }
264 } 313 }
265 if args.Verbose { 314 if args.Verbose {
266 log.Println("[DEBUG] rules directory:", args.RulesDir) 315 log.Println("[DEBUG] rules directory:", args.RulesDir)
@@ -302,7 +351,7 @@ func main() {
302 // load YARA rules 351 // load YARA rules
303 rulePath := path.Join(args.RulesDir, RulesFile) 352 rulePath := path.Join(args.RulesDir, RulesFile)
304 rules, err := loadRulesFile(rulePath) 353 rules, err := loadRulesFile(rulePath)
305 handleError(err, true) 354 handleError(err, "", true)
306 if args.Verbose { 355 if args.Verbose {
307 log.Println("[DEBUG] ruleset loaded:", rulePath) 356 log.Println("[DEBUG] ruleset loaded:", rulePath)
308 } 357 }
@@ -322,7 +371,7 @@ func main() {
322 371
323 // scan target 372 // scan target
324 if f, err := os.Stat(args.Positional.Target); os.IsNotExist(err) { 373 if f, err := os.Stat(args.Positional.Target); os.IsNotExist(err) {
325 handleError(err, true) 374 handleError(err, "", true)
326 } else { 375 } else {
327 if args.Verbose { 376 if args.Verbose {
328 log.Println("[DEBUG] scan workers:", args.Workers) 377 log.Println("[DEBUG] scan workers:", args.Workers)
@@ -387,7 +436,7 @@ func main() {
387 var matches yara.MatchRules 436 var matches yara.MatchRules
388 scanner := makeScanner(rules) 437 scanner := makeScanner(rules)
389 err := scanner.SetCallback(&matches).ScanFile(args.Positional.Target) 438 err := scanner.SetCallback(&matches).ScanFile(args.Positional.Target)
390 handleError(err, true) 439 handleError(err, "unable to scan target", true)
391 for _, match := range matches { 440 for _, match := range matches {
392 log.Println("[WARNING] match found:", args.Positional.Target, match.Rule) 441 log.Println("[WARNING] match found:", args.Positional.Target, match.Rule)
393 if args.Verbose { 442 if args.Verbose {
@@ -405,4 +454,13 @@ func main() {
405 log.Printf("[DEBUG] scanned %d files in %s\n", scannedFilesCount, endTime.Sub(startTime).String()) 454 log.Printf("[DEBUG] scanned %d files in %s\n", scannedFilesCount, endTime.Sub(startTime).String())
406 } 455 }
407 } 456 }
457
458 // delete temporary files
459 if strings.HasPrefix(args.RulesDir, tempDirPathPrefix) {
460 if args.Verbose {
461 log.Println("[DEBUG] deleting temporary folder:", args.RulesDir)
462 }
463 err := os.RemoveAll(args.RulesDir)
464 handleError(err, "unable to delete temporary folder", true)
465 }
408} 466}