-
Notifications
You must be signed in to change notification settings - Fork 103
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add quarantine check to scan for quarantine files and meddlesome proc…
…esses (#1333)
- Loading branch information
1 parent
38e80d1
commit c5342fb
Showing
3 changed files
with
324 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,236 @@ | ||
package checkups | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"os" | ||
"path/filepath" | ||
"runtime" | ||
"strings" | ||
|
||
"github.com/shirou/gopsutil/v3/process" | ||
) | ||
|
||
// quarantine: | ||
// Recursively scans common installation directories to to a given depth. | ||
// Reports and directories that have the word "quarantine" in their path and the number of files and their names they contain. | ||
// Warns if any files are found in the above directories. | ||
// Reports possible "meddlesome" processes for information purposes (does not fail due to proccesses running) | ||
|
||
// It's difficult to keep track of every possible Anti-Virus or EDRs quarantine directory, but they all seem | ||
// to have "quarantine" in their name. So we just look for that some where in the dir path. The suspicion | ||
// is that some programs will quarantine osquery. Unfortunalty, we typically can't see the names of the files | ||
// that were quarantined. So if we do find quarantined files, we'll fail and would ask the user to check and | ||
// see if osquery was quarantined. | ||
|
||
type quarantine struct { | ||
status Status | ||
summary string | ||
quarantineDirPathFilenames map[string][]string | ||
dirsChecked int | ||
} | ||
|
||
func (q *quarantine) Name() string { | ||
return "Quarantine" | ||
} | ||
|
||
func (q *quarantine) searchPathDepths() map[string]int { | ||
switch runtime.GOOS { | ||
case "windows": | ||
return map[string]int{ | ||
// Crowdstrike: C:\Windows\System32\Drivers\CrowdStrike\Quarantine | ||
`C:\Windows\System32\Drivers`: 3, | ||
// Malwarebytes: C:\ProgramData\Malwarebytes\MBAMService\Quarantine | ||
// Windows Defender: C:\ProgramData\Microsoft\Windows Defender\Quarantine | ||
`C:\ProgramData`: 3, | ||
} | ||
case "darwin": | ||
return map[string]int{ | ||
// Crowdstrike: /Library/Application Support/CrowdStrike/Falcon/Quarantine | ||
`/Library/Application Support`: 4, | ||
} | ||
case "linux": | ||
return map[string]int{ | ||
// Malwarebytes: /var/lib/mblinux/quarantine | ||
`/var/lib`: 3, | ||
} | ||
default: | ||
return make(map[string]int) | ||
} | ||
} | ||
|
||
func (q *quarantine) Run(ctx context.Context, extraFh io.Writer) error { | ||
q.quarantineDirPathFilenames = make(map[string][]string) | ||
|
||
var ( | ||
meddlesomeProcessPatterns = []string{ | ||
`crowdstrike`, | ||
`opswat`, | ||
`defend`, | ||
`defense`, | ||
`threat`, | ||
`virus`, | ||
`quarantine`, | ||
`snitch`, | ||
// carbon black possible processes | ||
`cbagent`, | ||
`carbonblack`, | ||
`repmgr`, | ||
`repwsc`, | ||
`cb.exe`, | ||
`cbdaemon`, | ||
`cbOsxSensorService`, | ||
} | ||
) | ||
|
||
fmt.Fprint(extraFh, "starting quarantine check\n") | ||
q.logMeddlesomeProccesses(ctx, extraFh, meddlesomeProcessPatterns) | ||
fmt.Fprintf(extraFh, "\nsearching for quarantined files:\n") | ||
|
||
for path, maxDepth := range q.searchPathDepths() { | ||
fileInfo, err := os.Stat(path) | ||
if err != nil { | ||
fmt.Fprintf(extraFh, "%s does not exist\n", path) | ||
continue | ||
} | ||
|
||
if !fileInfo.IsDir() { | ||
fmt.Fprintf(extraFh, "expected %s to be a directory, but was not\n", path) | ||
continue | ||
} | ||
|
||
q.checkDirs(extraFh, 0, maxDepth, path, "quarantine") | ||
} | ||
|
||
fmt.Fprintf(extraFh, "total directories checked: %d\n", q.dirsChecked) | ||
|
||
if len(q.quarantineDirPathFilenames) == 0 { | ||
q.status = Passing | ||
q.summary = "no quarantine directories found" | ||
fmt.Fprint(extraFh, "no quarantine directories found\n") | ||
return nil | ||
} | ||
|
||
fmt.Fprintf(extraFh, "quarantine directory paths and files:\n") | ||
|
||
totalQuarantinedFiles := 0 | ||
|
||
for path, fileNames := range q.quarantineDirPathFilenames { | ||
fmt.Fprintf(extraFh, "%s: %d files\n", path, len(fileNames)) | ||
totalQuarantinedFiles += len(fileNames) | ||
|
||
for _, fileName := range fileNames { | ||
fmt.Fprintf(extraFh, " %s\n", fileName) | ||
} | ||
} | ||
|
||
if totalQuarantinedFiles == 0 { | ||
q.status = Passing | ||
q.summary = "no files found in quarantine directories" | ||
return nil | ||
} | ||
|
||
q.status = Warning | ||
q.summary = fmt.Sprintf("found %d quarantined files", totalQuarantinedFiles) | ||
return nil | ||
} | ||
|
||
// Recursively scans dir to given max depth. Creates entry for each dir whose path contains the directoryKeyword. | ||
// Increments quarantine.quarantineCounts for each file found in folder and descendant folders. | ||
func (q *quarantine) checkDirs(extraFh io.Writer, currentDepth, maxDepth int, dirPath, directoryKeyword string) { | ||
if currentDepth > maxDepth { | ||
return | ||
} | ||
|
||
q.dirsChecked++ | ||
|
||
dirNameContainsKeyword := strings.Contains(strings.ToLower(dirPath), directoryKeyword) | ||
|
||
// add entry for each dir that contains the keyword | ||
if dirNameContainsKeyword { | ||
// create map entry if not exists | ||
if _, ok := q.quarantineDirPathFilenames[dirPath]; !ok { | ||
q.quarantineDirPathFilenames[dirPath] = make([]string, 0) | ||
} | ||
} | ||
|
||
dirEntries, err := os.ReadDir(dirPath) | ||
if err != nil { | ||
// some dirs, such as /Library/Application Support/com.apple.TCC can't be read even with sudo | ||
// have to give terminal FDA? | ||
// so just move on instead of failing | ||
fmt.Fprintf(extraFh, "failed to read %s: %s\n", dirPath, err) | ||
return | ||
} | ||
|
||
for _, dirEntry := range dirEntries { | ||
if dirEntry.IsDir() { | ||
q.checkDirs(extraFh, currentDepth+1, maxDepth, filepath.Join(dirPath, dirEntry.Name()), directoryKeyword) | ||
continue | ||
} | ||
|
||
if !dirNameContainsKeyword { | ||
// not in quarantine dir | ||
continue | ||
} | ||
|
||
// typically AVs will rename the file to a guid and store meta data some where | ||
// so just log the file count | ||
q.quarantineDirPathFilenames[dirPath] = append(q.quarantineDirPathFilenames[dirPath], dirEntry.Name()) | ||
} | ||
} | ||
|
||
func (q *quarantine) logMeddlesomeProccesses(ctx context.Context, extraFh io.Writer, containsSubStrings []string) error { | ||
fmt.Fprint(extraFh, "\npossilby meddlesome processes:\n") | ||
foundMeddlesomeProcesses := false | ||
|
||
ps, err := process.ProcessesWithContext(ctx) | ||
if err != nil { | ||
return fmt.Errorf("getting process list: %w", err) | ||
} | ||
|
||
for _, p := range ps { | ||
exe, _ := p.Exe() | ||
|
||
for _, s := range containsSubStrings { | ||
if !strings.Contains(strings.ToLower(exe), strings.ToLower(s)) { | ||
continue | ||
} | ||
foundMeddlesomeProcesses = true | ||
|
||
pMap := map[string]any{ | ||
"pid": p.Pid, | ||
"exe": naIfError(p.ExeWithContext(ctx)), | ||
"cmdline": naIfError(p.CmdlineSliceWithContext(ctx)), | ||
"create_time": naIfError(p.CreateTimeWithContext(ctx)), | ||
"ppid": naIfError(p.PpidWithContext(ctx)), | ||
"status": naIfError(p.StatusWithContext(ctx)), | ||
} | ||
|
||
fmt.Fprintf(extraFh, "%+v\n", pMap) | ||
} | ||
} | ||
|
||
if !foundMeddlesomeProcesses { | ||
fmt.Fprint(extraFh, "no meddlesome processes found\n") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (q *quarantine) Status() Status { | ||
return q.status | ||
} | ||
|
||
func (q *quarantine) Summary() string { | ||
return q.summary | ||
} | ||
|
||
func (q *quarantine) ExtraFileName() string { | ||
return "quarantine.log" | ||
} | ||
|
||
func (q *quarantine) Data() any { | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package checkups | ||
|
||
import ( | ||
"io" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func Test_quarantine_checkDirs(t *testing.T) { | ||
t.Parallel() | ||
|
||
const folderKeyword = "quarantine_checkup_test" | ||
|
||
tests := []struct { | ||
name string | ||
shouldPass bool | ||
pathsFunc func(t *testing.T) (string, map[string][]string) | ||
maxDepth int | ||
expectedDirsChecked int | ||
}{ | ||
{ | ||
name: "found quarantined files", | ||
pathsFunc: func(t *testing.T) (string, map[string][]string) { | ||
dir := t.TempDir() | ||
|
||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "1", folderKeyword, "2", "3", "4"), 0755)) | ||
require.NoError(t, os.WriteFile(filepath.Join(dir, "1", folderKeyword, "someFile"), nil, 0755)) | ||
require.NoError(t, os.WriteFile(filepath.Join(dir, "1", folderKeyword, "anotherFile"), nil, 0755)) | ||
|
||
require.NoError(t, os.WriteFile(filepath.Join(dir, "1", folderKeyword, "2", "3", "yetAnotherFile"), nil, 0755)) | ||
return dir, map[string][]string{ | ||
filepath.Join(dir, "1", folderKeyword): {"someFile", "anotherFile"}, | ||
filepath.Join(dir, "1", folderKeyword, "2"): {}, | ||
filepath.Join(dir, "1", folderKeyword, "2", "3"): {"yetAnotherFile"}, | ||
filepath.Join(dir, "1", folderKeyword, "2", "3", "4"): {}, | ||
} | ||
}, | ||
maxDepth: 10, | ||
expectedDirsChecked: 6, | ||
}, | ||
{ | ||
name: "doesnt exceed max depth", | ||
pathsFunc: func(t *testing.T) (string, map[string][]string) { | ||
dir := t.TempDir() | ||
|
||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "1", "2", folderKeyword), 0755)) | ||
require.NoError(t, os.WriteFile(filepath.Join(dir, "1", "not in special folder"), nil, 0755)) | ||
require.NoError(t, os.WriteFile(filepath.Join(dir, "1", "2", folderKeyword, "somefile"), nil, 0755)) | ||
return dir, map[string][]string{} | ||
}, | ||
maxDepth: 2, | ||
expectedDirsChecked: 3, | ||
}, | ||
{ | ||
name: "no notable files", | ||
pathsFunc: func(t *testing.T) (string, map[string][]string) { | ||
dir := t.TempDir() | ||
require.NoError(t, os.MkdirAll(filepath.Join(dir, "1", "2", "3"), 0755)) | ||
require.NoError(t, os.WriteFile(filepath.Join(dir, "1", "2", "dont care"), nil, 0755)) | ||
return dir, map[string][]string{} | ||
}, | ||
maxDepth: 10, | ||
expectedDirsChecked: 4, | ||
}, | ||
} | ||
|
||
for _, tt := range tests { | ||
tt := tt | ||
t.Run(tt.name, func(t *testing.T) { | ||
t.Parallel() | ||
q := quarantine{ | ||
quarantineDirPathFilenames: make(map[string][]string), | ||
} | ||
rootPath, expected := tt.pathsFunc(t) | ||
q.checkDirs(io.Discard, 0, tt.maxDepth, rootPath, folderKeyword) | ||
|
||
for path, files := range expected { | ||
val, ok := q.quarantineDirPathFilenames[path] | ||
require.True(t, ok, "path should be present in quarantine") | ||
require.ElementsMatch(t, files, val) | ||
} | ||
}) | ||
} | ||
} |