Skip to content

Commit

Permalink
Add additional checkups to flare and doctor commands (#1261)
Browse files Browse the repository at this point in the history
  • Loading branch information
directionless authored Jul 31, 2023
1 parent 7495db1 commit efbbb00
Show file tree
Hide file tree
Showing 8 changed files with 473 additions and 10 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ require (
require (
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v5 v5.0.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect
go.opentelemetry.io/proto/otlp v0.19.0 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE=
Expand Down
51 changes: 51 additions & 0 deletions pkg/debug/checkups/bboltdb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package checkups

import (
"context"
"errors"
"fmt"
"io"

"github.com/kolide/launcher/pkg/agent"
"github.com/kolide/launcher/pkg/agent/types"
)

type bboltdbCheckup struct {
k types.Knapsack
data any
}

func (c *bboltdbCheckup) Name() string {
return "bboltdb"
}

func (c *bboltdbCheckup) Run(_ context.Context, _ io.Writer) error {
db := c.k.BboltDB()
if db == nil {
return errors.New("no DB available")
}

stats, err := agent.GetStats(db)
if err != nil {
return fmt.Errorf("getting db stats: %w", err)
}

c.data = stats
return nil
}

func (c *bboltdbCheckup) ExtraFileName() string {
return ""
}

func (c *bboltdbCheckup) Status() Status {
return Informational
}

func (c *bboltdbCheckup) Summary() string {
return "N/A"
}

func (c *bboltdbCheckup) Data() any {
return c.data
}
4 changes: 4 additions & 0 deletions pkg/debug/checkups/checkups.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ func checkupsFor(k types.Knapsack, target targetBits) []checkupInt {
{&Connectivity{k: k}, doctorSupported | flareSupported},
{&Logs{k: k}, doctorSupported | flareSupported},
{&BinaryDirectory{}, doctorSupported | flareSupported},
{&launchdCheckup{}, doctorSupported | flareSupported},
{&runtimeCheckup{}, flareSupported},
{&enrollSecretCheckup{}, doctorSupported | flareSupported},
{&bboltdbCheckup{k: k}, flareSupported},
}

checkupsToRun := make([]checkupInt, 0)
Expand Down
143 changes: 143 additions & 0 deletions pkg/debug/checkups/enroll-secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package checkups

import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"runtime"

"github.com/golang-jwt/jwt/v5"
)

type enrollSecretCheckup struct {
summary string
status Status
}

func (c *enrollSecretCheckup) Name() string {
return "Enrollment Secret"
}

func (c *enrollSecretCheckup) Run(_ context.Context, extraFH io.Writer) error {
secretStatus := make(map[string]Status, 0)
secretSummary := make(map[string]string, 0)

for _, secretPath := range getSecretPaths() {
// Later on, we want to fall back to the _first_ secrets status. Set it here

st, summary := parseSecret(extraFH, secretPath)
secretStatus[secretPath] = st
secretSummary[secretPath] = summary

if c.status == Unknown || c.status == "" {
c.status = st
c.summary = summary
}
}

// Iterate over all the found secrets. If any pass, this passes. Otherwise fall back to the first secret.
// This is kinda roundabout, since this checkup is trying to support multiple possible paths
if c.status == Passing {
return nil
}
for secretPath, status := range secretStatus {
if status == Passing {
c.status = Passing
c.summary = secretSummary[secretPath]
}
}

if len(secretStatus) < 1 {
c.status = Erroring
c.summary = "No secrets for this platform"
return nil
}

return nil
}

func (c *enrollSecretCheckup) ExtraFileName() string {
return "secret-info.log"
}

func (c *enrollSecretCheckup) Status() Status {
return c.status
}

func (c *enrollSecretCheckup) Summary() string {
return c.summary
}

func (c *enrollSecretCheckup) Data() any {
return nil
}

// getSecretPaths returns potential platform default secret path. It should probably get folded into flags, but I'm not
// quite sure how yet.
func getSecretPaths() []string {
switch runtime.GOOS {
case "darwin":
return []string{"/etc/kolide-k2/secret"}
case "linux":
return []string{"/etc/kolide-k2/secret"}
case "windows":
return []string{"C:\\Program Files\\Kolide\\Launcher-kolide-k2\\conf\\secret"}
}

return nil
}

func parseSecret(extraFH io.Writer, secretPath string) (Status, string) {
fmt.Fprintf(extraFH, "%s:\n", secretPath)
defer fmt.Fprintf(extraFH, "\n\n")

secretFH, err := os.Open(secretPath)
switch {
case os.IsNotExist(err):
fmt.Fprintf(extraFH, "does not exist\n")
return Failing, "does not exist"
case os.IsPermission(err):
fmt.Fprintf(extraFH, "permission denied (might be running as user)\n")
return Informational, "permission denied (might be running as user)"
case err != nil:
fmt.Fprintf(extraFH, "unknown error: %s\n", err)
return Erroring, fmt.Sprintf("unknown error: %s", err)
}

// If we can read the secret, let's try to extract the munemo
tokenRaw, err := io.ReadAll(secretFH)
if err != nil {
fmt.Fprintf(extraFH, "%s: unable to read: %s\n", secretPath, err)
return Failing, fmt.Sprintf("unable to read: %s", err)
}

// We do not have the key, and thus CANNOT verify. So this is ParseUnverified
token, _, err := new(jwt.Parser).ParseUnverified(string(tokenRaw), jwt.MapClaims{})
if err != nil {
fmt.Fprintf(extraFH, "Error parsing JWT:\n%s\n", err)
return Failing, fmt.Sprintf("cannot jwt parse: %s", err)
}

claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
fmt.Fprintf(extraFH, "no jwt claims\n")
return Failing, "jwt has no claims"
}

// Print the claims into our extra data
fmt.Fprintf(extraFH, "---\n")
if err := json.NewEncoder(extraFH).Encode(claims); err != nil {
fmt.Fprintf(extraFH, "Cannot json encode: %s\n", err)
}
fmt.Fprintf(extraFH, "---\n")

// Expect the claims to have an organization
org, ok := claims["organization"]
if !ok {
return Failing, "no organization in claim"
}

return Passing, fmt.Sprintf("claim for %s", org)
}
115 changes: 115 additions & 0 deletions pkg/debug/checkups/launchd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package checkups

import (
"archive/zip"
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
)

const (
launchdPlistPath = "/Library/LaunchDaemons/com.kolide-k2.launcher.plist"
launchdServiceName = "system/com.kolide-k2.launcher"
)

type launchdCheckup struct {
status Status
summary string
}

func (c *launchdCheckup) Name() string {
if runtime.GOOS != "darwin" {
return ""
}

return "Launchd"
}

func (c *launchdCheckup) Run(ctx context.Context, extraWriter io.Writer) error {
// Check that the plist exists (this uses open not stat, because we also want to copy it)
launchdPlist, err := os.Open(launchdPlistPath)
if os.IsNotExist(err) {
c.status = Failing
c.summary = "plist does not exist"
return nil
} else if err != nil {
c.status = Failing
c.summary = fmt.Sprintf("error reading %s: %s", launchdPlistPath, err)
return nil
}
defer launchdPlist.Close()

extraZip := zip.NewWriter(extraWriter)
defer extraZip.Close()

zippedPlist, err := extraZip.Create(filepath.Base(launchdPlistPath))
if err != nil {
c.status = Erroring
c.summary = fmt.Sprintf("unable to create extra information: %s", err)
return nil
}

if _, err := io.Copy(zippedPlist, launchdPlist); err != nil {
c.status = Erroring
c.summary = fmt.Sprintf("unable to write extra information: %s", err)
return nil

}

// run launchctl to check status
var printOut bytes.Buffer

cmd := exec.CommandContext(ctx, "/bin/launchctl", "print", launchdServiceName)
cmd.Stdout = &printOut
cmd.Stderr = &printOut
if err := cmd.Run(); err != nil {
c.status = Failing
c.summary = fmt.Sprintf("error running launchctl print: %s", err)
return nil
}

zippedOut, err := extraZip.Create("launchctl-print.txt")
if err != nil {
c.status = Erroring
c.summary = fmt.Sprintf("unable to create launchctl-print.txt: %s", err)
return nil
}
if _, err := zippedOut.Write(printOut.Bytes()); err != nil {
c.status = Erroring
c.summary = fmt.Sprintf("unable to write launchctl-print.txt: %s", err)
return nil

}

if !strings.Contains(printOut.String(), "state = running") {
c.status = Failing
c.summary = fmt.Sprintf("state not active")
return nil
}

c.status = Passing
c.summary = "state is running"
return nil
}

func (c *launchdCheckup) ExtraFileName() string {
return "launchd.zip"
}

func (c *launchdCheckup) Status() Status {
return c.status
}

func (c *launchdCheckup) Summary() string {
return c.summary
}

func (c *launchdCheckup) Data() any {
return nil
}
Loading

0 comments on commit efbbb00

Please sign in to comment.