diff options
| author | Mathieu Deous | 2022-05-02 20:18:23 +0200 |
|---|---|---|
| committer | GitHub | 2022-05-02 20:18:23 +0200 |
| commit | 48936efa96ae17295be4e0a71be3294f0ec6aef8 (patch) | |
| tree | f4e69551f1368aa048edf46b7b061600f3668329 /main.go | |
| parent | bbc738e16f8b637afde58d65196374af98a5e0e2 (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 | ||
| 3 | import ( | 3 | import ( |
| 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 | ||
| 20 | const RulesURI = "https://raw.githubusercontent.com/jvoisin/php-malware-finder/master/php-malware-finder/" | 22 | const ( |
| 21 | const RulesFile = "php.yar" | 23 | RulesURI = "https://raw.githubusercontent.com/jvoisin/php-malware-finder/master/php-malware-finder/data" |
| 22 | const DefaultDir = "/etc/phpmalwarefinder" | 24 | RulesFile = "php.yar" |
| 23 | const ScanMaxDuration = time.Duration(60) | 25 | ScanMaxDuration = time.Duration(60) |
| 24 | const TooShort = "TooShort" | 26 | TooShort = "TooShort" |
| 25 | const TooShortMaxLines = 2 | 27 | TooShortMaxLines = 2 |
| 26 | const TooShortMinChars = 300 | 28 | TooShortMinChars = 300 |
| 27 | const DangerousMatchWeight = 2 | 29 | DangerousMatchWeight = 2 |
| 28 | const DangerousMinScore = 3 | 30 | DangerousMinScore = 3 |
| 29 | const FileBufferSize = 32 * 1024 // 32KB | 31 | FileBufferSize = 32 * 1024 // 32KB |
| 30 | const YaraMaxThreads = 32 | 32 | YaraMaxThreads = 32 |
| 33 | TempDirPrefix = "pmf-" | ||
| 34 | ) | ||
| 31 | 35 | ||
| 32 | var args struct { // command-line arguments specs using github.com/jessevdk/go-flags | 36 | var ( |
| 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 |
| 48 | var scanFlags yara.ScanFlags | 52 | } `positional-args:"yes"` |
| 49 | var stoppedWorkers int | 53 | } |
| 50 | var lineFeed = []byte{'\n'} | 54 | scanFlags yara.ScanFlags |
| 51 | var 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": {}, |
| 57 | var excludedDirs = [...]string{ | 61 | "NonPrintableChars": {}, |
| 58 | "/.git/", "/.hg/", "/.svn/", "/.CVS/", | 62 | } |
| 59 | } | 63 | excludedDirs = [...]string{ |
| 60 | var excludedExts = map[string]struct{}{} | 64 | "/.git/", "/.hg/", "/.svn/", "/.CVS/", |
| 61 | var 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 |
| 67 | var imageExts = [...]string{ | 71 | ".so", ".dll", ".bin", ".exe", ".bundle", // binaries |
| 68 | ".png", ".jpg", ".jpeg", ".gif", ".svg", ".bmp", ".ico", | 72 | } |
| 69 | } | 73 | imageExts = [...]string{ |
| 70 | var 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. |
| 73 | func handleError(err error, exit bool) { | 90 | func 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. | ||
| 104 | func 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. |
| 84 | func updateRules() { | 129 | func 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. |
| 123 | func fileStats(filepath string) (int, int, error) { | 171 | func 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. |
| 146 | func makeScanner(rules *yara.Rules) *yara.Scanner { | 196 | func 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) { | |||
| 249 | func main() { | 299 | func 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 | } |
