Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
[![Go Version](https://img.shields.io/badge/Go-1.24+-00ADD8?style=flat-square&logo=go)](https://golang.org)
[![License](https://img.shields.io/github/license/codeyourweb/fastfinder?style=flat-square)](LICENSE)
[![Release](https://img.shields.io/github/v/release/codeyourweb/fastfinder?style=flat-square)](https://github.com/codeyourweb/fastfinder/releases)
[![Build Status](https://img.shields.io/github/actions/workflow/status/codeyourweb/fastfinder/go_build_windows.yml?style=flat-square&label=Windows)](https://github.com/codeyourweb/fastfinder/actions)
[![Build Status](https://img.shields.io/github/actions/workflow/status/codeyourweb/fastfinder/go_build_linux.yml?style=flat-square&label=Linux)](https://github.com/codeyourweb/fastfinder/actions)
[![Build Status](https://img.shields.io/github/actions/workflow/status/codeyourweb/fastfinder/go_build_windows_amd64.yml?style=flat-square&label=Windows)](https://github.com/codeyourweb/fastfinder/actions)
[![Build Status](https://img.shields.io/github/actions/workflow/status/codeyourweb/fastfinder/go_build_linux_amd64.yml?style=flat-square&label=Linux)](https://github.com/codeyourweb/fastfinder/actions)
[![Platform Support](https://img.shields.io/badge/Platform-Windows%20%7C%20Linux-brightgreen?style=flat-square)](#installation)

## ✨ Overview
Expand Down Expand Up @@ -148,6 +148,7 @@ options:
findInRemovableDrives: true # enumerate removable drive content
findInNetworkDrives: true # enumerate network drive content
findInCDRomDrives: true # enumerate physical CD-ROM and mounted iso / vhd...
findInMemory: true # check for results in processes memory
output:
copyMatchingFiles: true # create a copy of every matching file
base64Files: true # base64 matched content before copy
Expand Down
21 changes: 17 additions & 4 deletions config_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"os"
"os/exec"
"path/filepath"
"testing"
)
Expand Down Expand Up @@ -108,14 +109,26 @@ func TestConfigurationMissingRequired(t *testing.T) {

// TestConfigurationEmpty tests handling of empty configuration
func TestConfigurationEmpty(t *testing.T) {
if os.Getenv("TEST_CONFIGURATION_EMPTY_SUBPROCESS") == "1" {
tmpFile := os.Getenv("TEST_CONFIGURATION_EMPTY_FILE")
var config Configuration
config.getConfiguration(tmpFile)
return
}

tmpFile := filepath.Join(t.TempDir(), "empty_config.yml")
os.WriteFile(tmpFile, []byte(""), 0644)

var config Configuration
config.getConfiguration(tmpFile)
cmd := exec.Command(os.Args[0], "-test.run=TestConfigurationEmpty")
cmd.Env = append(os.Environ(), "TEST_CONFIGURATION_EMPTY_SUBPROCESS=1", "TEST_CONFIGURATION_EMPTY_FILE="+tmpFile)
err := cmd.Run()

// Verify no panic occurred
t.Log("Empty configuration handled gracefully")
// We expect an exit error here because LogFatal calls os.Exit(1)
if e, ok := err.(*exec.ExitError); ok && !e.Success() {
t.Log("Empty configuration triggered exit as expected")
return
}
t.Fatalf("process ran with err %v, want exit status 1", err)
}

// TestConfigurationYARAWithRC4 tests YARA section with RC4 encryption
Expand Down
29 changes: 6 additions & 23 deletions configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ import (
"gopkg.in/yaml.v3"
)

type ConfigurationObject struct {
Line int

Configuration
}

type Configuration struct {
Input Input `yaml:"input"`
Options Options `yaml:"options"`
Expand All @@ -45,7 +39,7 @@ type Options struct {
FindInRemovableDrives bool `yaml:"findInRemovableDrives"`
FindInNetworkDrives bool `yaml:"findInNetworkDrives"`
FindInCDRomDrives bool `yaml:"findInCDRomDrives"`
ScanMemory bool `yaml:"scanMemory"`
FindInMemory bool `yaml:"findInMemory"`
}

type Output struct {
Expand All @@ -60,17 +54,6 @@ type AdvancedParameters struct {
CleanMemoryIfFileGreaterThanSize int `yaml:"cleanMemoryIfFileGreaterThanSize"`
}

func (i *ConfigurationObject) UnmarshalYAML(value *yaml.Node) error {
err := value.Decode(&i.Configuration)
if err != nil {
return err
}

i.Line = value.Line

return nil
}

func (c *Configuration) getConfiguration(configFile string) *Configuration {
var yamlContent []byte
var err error
Expand Down Expand Up @@ -107,15 +90,14 @@ func (c *Configuration) getConfiguration(configFile string) *Configuration {
yamlContent = RC4Cipher(yamlContent, BUILDER_RC4_KEY)
}

var o ConfigurationObject
err = yaml.Unmarshal(yamlContent, &o)
decoder := yaml.NewDecoder(bytes.NewReader(yamlContent))
decoder.KnownFields(true)
err = decoder.Decode(c)

if err != nil {
LogFatal(fmt.Sprintf("%s - %v", configFile, err))
}

*c = o.Configuration

// check for specific user configuration params inconsistencies
if len(c.Input.Path) == 0 || (len(c.Input.Content.Grep) == 0 && len(c.Input.Content.Yara) == 0 && len(c.Input.Content.Checksum) == 0) {
c.Options.ContentMatchDependsOnPathMatch = false
Expand Down Expand Up @@ -203,5 +185,6 @@ func (c *Configuration) getConfiguration(configFile string) *Configuration {
}
}

LogMessage(LOG_INFO, "Configuration loaded")
return c
}
}
32 changes: 31 additions & 1 deletion event_forwarding.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type EventForwarder struct {
currentFilePath string
lastRotation time.Time
fileMutex sync.Mutex
wg sync.WaitGroup
}

// FastFinderEvent represents an event to be forwarded
Expand Down Expand Up @@ -133,6 +134,7 @@ func InitializeEventForwarding(config *ForwardingConfig) error {
httpClient: httpClient,
}

eventForwarder.wg.Add(1)
// Start the forwarding goroutine
go eventForwarder.forwardingLoop()

Expand Down Expand Up @@ -190,6 +192,30 @@ func ForwardAlertEvent(ruleName, filePath string, fileSize int64, fileHash strin
ForwardEvent("alert", "high", fmt.Sprintf("YARA rule match: %s in %s", ruleName, filePath), metadata)
}

// ForwardGrepMatchEvent forwards a Grep match event
func ForwardGrepMatchEvent(pattern, filePath string, fileSize int64, metadata map[string]string) {
if metadata == nil {
metadata = make(map[string]string)
}
metadata["grep_pattern"] = pattern
metadata["file_path"] = filePath
metadata["file_size"] = fmt.Sprintf("%d", fileSize)

ForwardEvent("alert", "high", fmt.Sprintf("Grep match: %s in %s", pattern, filePath), metadata)
}

// ForwardChecksumMatchEvent forwards a Checksum match event
func ForwardChecksumMatchEvent(checksum, filePath string, fileSize int64, metadata map[string]string) {
if metadata == nil {
metadata = make(map[string]string)
}
metadata["checksum"] = checksum
metadata["file_path"] = filePath
metadata["file_size"] = fmt.Sprintf("%d", fileSize)

ForwardEvent("alert", "high", fmt.Sprintf("Checksum match: %s in %s", checksum, filePath), metadata)
}

// ForwardScanCompleteEvent forwards scan completion statistics
func ForwardScanCompleteEvent(filesScanned, matchesFound, errorsEncountered int, duration time.Duration) {
if eventForwarder == nil {
Expand Down Expand Up @@ -243,6 +269,7 @@ func (ef *EventForwarder) shouldForwardEvent(eventType, severity string) bool {

// forwardingLoop runs the periodic event forwarding
func (ef *EventForwarder) forwardingLoop() {
defer ef.wg.Done()
ticker := time.NewTicker(time.Duration(ef.config.FlushTime) * time.Second)
defer ticker.Stop()

Expand Down Expand Up @@ -466,11 +493,14 @@ func (ef *EventForwarder) cleanOldFiles() {
// StopEventForwarding stops the event forwarding system
func StopEventForwarding() {
if eventForwarder != nil {
close(eventForwarder.stopChannel)
eventForwarder.wg.Wait()

// Close current file if open
if eventForwarder.currentFile != nil {
eventForwarder.currentFile.Close()
eventForwarder.currentFile = nil
}
close(eventForwarder.stopChannel)
eventForwarder = nil
}
}
2 changes: 1 addition & 1 deletion examples/CISA-AA21-259A/CISA-AA21-259A.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ options:
findInRemovableDrives: false
findInNetworkDrives: false
findInCDRomDrives: false
scanMemory: false
findInMemory: false
output:
copyMatchingFiles: false
base64Files: false
Expand Down
2 changes: 1 addition & 1 deletion examples/React2Shell/react2shell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ options:
findInRemovableDrives: true
findInNetworkDrives: true
findInCDRomDrives: true
scanMemory: true
findInMemory: true
output:
copyMatchingFiles: false
base64Files: false
Expand Down
2 changes: 1 addition & 1 deletion examples/example_configuration_api_triage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ options:
findInRemovableDrives: false
findInNetworkDrives: false
findInCDRomDrives: false
scanMemory: false
findInMemory: false
output:
copyMatchingFiles: false
base64Files: false
Expand Down
2 changes: 1 addition & 1 deletion examples/example_configuration_distant.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ options:
findInRemovableDrives: false
findInNetworkDrives: false
findInCDRomDrives: false
scanMemory: false
findInMemory: false
output:
copyMatchingFiles: false
base64Files: false
Expand Down
2 changes: 1 addition & 1 deletion examples/example_configuration_docker_triage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ options:
findInRemovableDrives: false
findInNetworkDrives: false
findInCDRomDrives: false
scanMemory: false
findInMemory: false
output:
copyMatchingFiles: true
base64Files: true
Expand Down
2 changes: 1 addition & 1 deletion examples/example_configuration_linux.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ options:
findInRemovableDrives: false
findInNetworkDrives: false
findInCDRomDrives: false
scanMemory: true
findInMemory: true
output:
copyMatchingFiles: false
base64Files: false
Expand Down
2 changes: 1 addition & 1 deletion examples/example_configuration_windows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ options:
findInRemovableDrives: false
findInNetworkDrives: false
findInCDRomDrives: false
scanMemory: true
findInMemory: true
output:
copyMatchingFiles: false
base64Files: false
Expand Down
2 changes: 1 addition & 1 deletion examples/linux-fontonlake/eset_fontonlake_linux.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ options:
findInRemovableDrives: false
findInNetworkDrives: false
findInCDRomDrives: false
scanMemory: false
findInMemory: false
output:
copyMatchingFiles: false
base64Files: false
Expand Down
1 change: 1 addition & 0 deletions examples/log4j_vuln_checker/config-log4j_vuln_checker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ options:
findInRemovableDrives: false
findInNetworkDrives: false
findInCDRomDrives: false
findInMemory: true
output:
copyMatchingFiles: false
base64Files: false
Expand Down
2 changes: 1 addition & 1 deletion examples/proxyshell/drophell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ options:
findInRemovableDrives: false
findInNetworkDrives: false
findInCDRomDrives: false
scanMemory: true
findInMemory: true
output:
copyMatchingFiles: false
base64Files: false
Expand Down
21 changes: 16 additions & 5 deletions finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,6 @@ func FindInFilesContent(files *[]string, patterns []string, rules *yara.Rules, h
// handle file content and checksum match
for _, m := range CheckFileChecksumAndContent(path, b, hashList, patterns) {
if !Contains(matchingFiles, m) {
LogMessage(LOG_ALERT, "(ALERT)", "File content match on:", path)
matchingFiles = append(matchingFiles, m)
}
}
Expand Down Expand Up @@ -117,7 +116,6 @@ func FindInFilesContent(files *[]string, patterns []string, rules *yara.Rules, h
// handle file content and checksum match for each file in the archive
for _, m := range CheckFileChecksumAndContent(path, body, hashList, patterns) {
if !Contains(matchingFiles, m) {
LogMessage(LOG_ALERT, "(ALERT)", "File content match on:", path)
matchingFiles = append(matchingFiles, m)
}
}
Expand Down Expand Up @@ -176,6 +174,7 @@ func checkForChecksum(path string, content []byte, hashList []string) (matchingF
for _, c := range hashs {
if Contains(hashList, c) && !Contains(matchingFiles, path) {
LogMessage(LOG_ALERT, "(ALERT)", "Checksum match:", c, "in", path)
ForwardChecksumMatchEvent(c, path, int64(len(content)), nil)
matchingFiles = append(matchingFiles, path)
}
}
Expand All @@ -186,10 +185,22 @@ func checkForChecksum(path string, content []byte, hashList []string) (matchingF
// checkForStringPattern check if file content matches any specified pattern
func checkForStringPattern(path string, content []byte, patterns []string) (matchingFiles []string) {
LogMessage(LOG_VERBOSE, "(SCAN)", "Checking grep patterns in", path)
contentStr := string(content)
lines := strings.Split(contentStr, "\n")

for _, expression := range patterns {
if strings.Contains(string(content), expression) {
LogMessage(LOG_ALERT, "(ALERT)", "Grep match:", expression, "in", path)
matchingFiles = append(matchingFiles, path)
for i, line := range lines {
if strings.Contains(line, expression) {
LogMessage(LOG_ALERT, "(ALERT)", "Grep match:", expression, "in", path, "at line", fmt.Sprintf("%d", i+1))

metadata := map[string]string{
"line_number": fmt.Sprintf("%d", i+1),
}
ForwardGrepMatchEvent(expression, path, int64(len(content)), metadata)

matchingFiles = append(matchingFiles, path)
break
}
}
}
return matchingFiles
Expand Down
2 changes: 1 addition & 1 deletion logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const (
LOG_VERBOSE = 5 // Full verbosity (all messages)
)

var loggingVerbosity int = 3
var loggingVerbosity int = 4
var loggingPath string = ""
var loggingFile *os.File
var unitTesting bool
Expand Down
15 changes: 10 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
"github.com/hillu/go-yara/v4"
)

const FASTFINDER_VERSION = "3.0.0"
const FASTFINDER_VERSION = "3.6.0"
const YARA_VERSION = "4.5.5"
const BUILDER_RC4_KEY = ">Õ°ªKb{¡§ÌB$lMÕ±9l.tòÑ馨¿"

Expand All @@ -33,7 +33,7 @@ func main() {
pConfigPath := parser.String("c", "configuration", &argparse.Options{Required: false, Default: "", Help: "Fastfind configuration file"})
pSfxPath := parser.String("b", "build", &argparse.Options{Required: false, Help: "Output a standalone package with configuration and rules in a single binary"})
pSilentMode := parser.Flag("s", "silent", &argparse.Options{Required: false, Help: "Silent mode - run without any visible window or console"})
pLogVerbosity := parser.Int("v", "verbosity", &argparse.Options{Required: false, Default: 3, Help: "File log verbosity \n\t\t\t\t | 1: Only alerts\n\t\t\t\t | 2: Alerts and warnings\n\t\t\t\t | 3: Alerts,warnings and errors\n\t\t\t\t | 4: Alerts,warnings,errors and I/O operations\n\t\t\t\t | 5: Full verbosity)\n\t\t\t\t"})
pLogVerbosity := parser.Int("v", "verbosity", &argparse.Options{Required: false, Default: 4, Help: "File log verbosity \n\t\t\t\t | 1: Only alerts\n\t\t\t\t | 2: Alerts and warnings\n\t\t\t\t | 3: Alerts,warnings and errors\n\t\t\t\t | 4: Alerts,warnings,errors and I/O operations\n\t\t\t\t | 5: Full verbosity)\n\t\t\t\t"})
pTriage := parser.Flag("t", "triage", &argparse.Options{Required: false, Default: false, Help: "Triage mode (infinite run - scan every new file in the input path directories)"})
pRootPath := parser.String("r", "root", &argparse.Options{Required: false, Default: "", Help: "Scan root path (override drive enumeration to scan specific directory)"})

Expand Down Expand Up @@ -150,7 +150,7 @@ func MainFastfinderRoutine(config Configuration, pConfigPath string, pNoAdvUI bo
}

// Memory Scan
if config.Options.ScanMemory {
if config.Options.FindInMemory {
ScanMemory(config, rules)
}

Expand Down Expand Up @@ -266,6 +266,11 @@ func MainFastfinderRoutine(config Configuration, pConfigPath string, pNoAdvUI bo
// Wait for matches collection to complete
<-matchesDone

// Update stats
totalFilesScanned += int(pipeline.GetFilesScanned())
totalErrorsEncountered += int(pipeline.GetErrorsEncountered())
totalMatchesFound += len(matchingFiles)

// listing and copy matching files
LogMessage(LOG_INFO, "(INFO)", "scan finished in", basePath)
if len(matchingFiles) > 0 {
Expand Down Expand Up @@ -297,8 +302,8 @@ func MainFastfinderRoutine(config Configuration, pConfigPath string, pNoAdvUI bo
StopEventForwarding()
}

LogMessage(LOG_INFO, "(INFO)", fmt.Sprintf("Scan completed in %v", scanDuration))
LogMessage(LOG_INFO, "(INFO)", fmt.Sprintf("Files scanned: %d, Matches found: %d, Errors: %d",
LogMessage(LOG_ALERT, "(INFO)", fmt.Sprintf("Scan completed in %v", scanDuration))
LogMessage(LOG_ALERT, "(INFO)", fmt.Sprintf("Files scanned: %d, Matches found: %d, Errors: %d",
totalFilesScanned, totalMatchesFound, totalErrorsEncountered))

ExitProgram(0, !UIactive)
Expand Down
Loading
Loading