Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add quarantine check to scan for quarantine files and meddlesome processes #1333

Merged
merged 13 commits into from
Sep 7, 2023
1 change: 1 addition & 0 deletions pkg/debug/checkups/checkups.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func checkupsFor(k types.Knapsack, target targetBits) []checkupInt {
{&osqueryCheckup{k: k}, doctorSupported | flareSupported},
{&launcherFlags{}, doctorSupported | flareSupported},
{&gnomeExtensions{}, doctorSupported | flareSupported},
{&quarantine{}, doctorSupported | flareSupported},
}

checkupsToRun := make([]checkupInt, 0)
Expand Down
236 changes: 236 additions & 0 deletions pkg/debug/checkups/quarantine.go
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"
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
}

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{
RebeccaMahany marked this conversation as resolved.
Show resolved Hide resolved
`crowdstrike`,
`opswat`,
`defend`,
`defense`,
`threat`,
`virus`,
`quarantine`,
`snitch`,
// carbon black possible processes
`cbagent`,
`carbonblack`,
`repmgr`,
`repwsc`,
`cb.exe`,
`cbdaemon`,
`cbOsxSensorService`,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know what carbonblack's process name is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

googling revealed many possibilities, I added all the ones that looked promising

)

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
}
87 changes: 87 additions & 0 deletions pkg/debug/checkups/quarantine_test.go
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)
}
})
}
}
Loading