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
156 changes: 156 additions & 0 deletions cmd/aguara/commands/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package commands

import (
"encoding/json"
"fmt"
"os"
"strings"

"github.com/garagon/aguara/internal/incident"
"github.com/spf13/cobra"
)

var (
flagCheckPath string
flagCheckIncludeCaches bool
)

var checkCmd = &cobra.Command{
Use: "check",
Short: "Check for compromised Python packages and persistence artifacts",
Long: `Scan installed Python packages for known compromised versions, malicious .pth
files, and persistence backdoors. Reports which credential files are at risk.`,
RunE: runCheck,
}

func init() {
checkCmd.Flags().StringVar(&flagCheckPath, "path", "", "Path to site-packages directory (default: auto-discover)")
checkCmd.Flags().BoolVar(&flagCheckIncludeCaches, "include-caches", false, "Also check pip/uv cache directories")
rootCmd.AddCommand(checkCmd)
}

func runCheck(cmd *cobra.Command, args []string) error {
result, err := incident.Check(incident.CheckOptions{
Path: flagCheckPath,
IncludeCaches: flagCheckIncludeCaches,
})
if err != nil {
return err
}

if flagFormat == "json" {
return writeCheckJSON(result)
}
return writeCheckTerminal(result)
}

func writeCheckJSON(result *incident.CheckResult) error {
w := os.Stdout
if flagOutput != "" {
f, err := os.Create(flagOutput)
if err != nil {
return err
}
defer func() { _ = f.Close() }()
w = f
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(result)
}

func writeCheckTerminal(result *incident.CheckResult) error {
fmt.Printf("\nScanning Python environment: %s\n", result.Environment)
fmt.Printf("Packages read: %d | .pth files scanned: %d\n\n", result.PackagesRead, result.PthScanned)

if len(result.Findings) == 0 {
green := "\033[32m"
reset := "\033[0m"
if flagNoColor {
green = ""
reset = ""
}
fmt.Printf(" %s\u2714 No compromised packages or artifacts found.%s\n\n", green, reset)
return nil
}

red := "\033[31m"
yellow := "\033[33m"
bold := "\033[1m"
dim := "\033[2m"
reset := "\033[0m"
cyan := "\033[36m"
if flagNoColor {
red = ""
yellow = ""
bold = ""
dim = ""
reset = ""
cyan = ""
}

for _, f := range result.Findings {
var sevColor string
switch f.Severity {
case incident.SevCritical:
sevColor = red + bold
case incident.SevWarning:
sevColor = yellow
default:
sevColor = cyan
}
fmt.Printf("%s%-10s%s %s\n", sevColor, f.Severity, reset, f.Title)
if f.Path != "" {
fmt.Printf(" %sPath: %s%s\n", dim, f.Path, reset)
}
if f.Detail != "" {
fmt.Printf(" %s%s%s\n", dim, f.Detail, reset)
}
fmt.Println()
}

// Credentials at risk
atRisk := 0
for _, c := range result.Credentials {
if c.Exists {
atRisk++
}
}
if atRisk > 0 {
fmt.Printf("%sCredentials at risk:%s\n", bold, reset)
for _, c := range result.Credentials {
if c.Exists {
fmt.Printf(" %-30s %sEXISTS%s %s%s%s\n", c.Path, red, reset, dim, c.Guidance, reset)
}
}
fmt.Println()
}

// Action guidance
fmt.Printf("%sAction required:%s\n", bold, reset)
fmt.Println(" 1. Run 'aguara clean' to remove malicious files")
fmt.Println(" 2. Rotate ALL credentials listed above")
fmt.Println(" 3. If running K8s: kubectl get pods -n kube-system | grep node-setup")

// Build summary line
critCount := 0
warnCount := 0
for _, f := range result.Findings {
switch f.Severity {
case incident.SevCritical:
critCount++
case incident.SevWarning:
warnCount++
}
}
var parts []string
if critCount > 0 {
parts = append(parts, fmt.Sprintf("%s%d critical%s", red, critCount, reset))
}
if warnCount > 0 {
parts = append(parts, fmt.Sprintf("%s%d warning%s", yellow, warnCount, reset))
}
fmt.Printf("\n%s\n", strings.Join(parts, " \u00b7 "))

return nil
}
170 changes: 170 additions & 0 deletions cmd/aguara/commands/clean.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package commands

import (
"bufio"
"encoding/json"
"fmt"
"os"
"strings"

"github.com/garagon/aguara/internal/incident"
"github.com/spf13/cobra"
)

var (
flagCleanDryRun bool
flagCleanPurgeCaches bool
flagCleanYes bool
)

var cleanCmd = &cobra.Command{
Use: "clean",
Short: "Remove compromised packages, malicious files, and persistence artifacts",
Long: `Detects and removes compromised Python packages, quarantines malicious .pth files,
and disables persistence backdoors. Use --dry-run to preview without changes.`,
RunE: runClean,
}

func init() {
cleanCmd.Flags().BoolVar(&flagCleanDryRun, "dry-run", false, "Show what would be removed without making changes")
cleanCmd.Flags().BoolVar(&flagCleanPurgeCaches, "purge-caches", false, "Also purge pip/uv package caches")
cleanCmd.Flags().BoolVar(&flagCleanYes, "yes", false, "Skip confirmation prompt")
rootCmd.AddCommand(cleanCmd)
}

func runClean(cmd *cobra.Command, args []string) error {
// First run check to see what we're dealing with
checkResult, err := incident.Check(incident.CheckOptions{
Path: flagCheckPath,
IncludeCaches: flagCleanPurgeCaches,
})
if err != nil {
return err
}

if len(checkResult.Findings) == 0 {
fmt.Println("\n \033[32m\u2714 No compromised packages or artifacts found.\033[0m")
return nil
}

// Show what was found
fmt.Printf("\nFound %d issues to clean:\n\n", len(checkResult.Findings))
for i, f := range checkResult.Findings {
fmt.Printf(" [%d] %s - %s\n", i+1, f.Severity, f.Title)
if f.Path != "" {
fmt.Printf(" %s\n", f.Path)
}
}
fmt.Println()

if flagCleanDryRun {
fmt.Println("No changes made (dry run).")
return nil
}

// Confirm unless --yes
if !flagCleanYes {
fmt.Print("Proceed with cleanup? [y/N] ")
reader := bufio.NewReader(os.Stdin)
answer, _ := reader.ReadString('\n')
answer = strings.TrimSpace(strings.ToLower(answer))
if answer != "y" && answer != "yes" {
fmt.Println("Aborted.")
return nil
}
}

// Run cleanup
result, err := incident.Clean(incident.CleanOptions{
DryRun: false,
PurgeCaches: flagCleanPurgeCaches,
CheckOpts: incident.CheckOptions{
Path: flagCheckPath,
IncludeCaches: flagCleanPurgeCaches,
},
})
if err != nil {
return err
}

if flagFormat == "json" {
return writeCleanJSON(result)
}
return writeCleanTerminal(result)
}

func writeCleanJSON(result *incident.CleanResult) error {
w := os.Stdout
if flagOutput != "" {
f, err := os.Create(flagOutput)
if err != nil {
return err
}
defer func() { _ = f.Close() }()
w = f
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(result)
}

func writeCleanTerminal(result *incident.CleanResult) error {
red := "\033[31m"
green := "\033[32m"
bold := "\033[1m"
dim := "\033[2m"
reset := "\033[0m"
if flagNoColor {
red = ""
green = ""
bold = ""
dim = ""
reset = ""
}

done := 0
for i, a := range result.Actions {
status := green + "\u2714" + reset
if a.Error != "" {
status = red + "\u2716" + reset
}
if !a.Done && a.Error == "" {
status = dim + "-" + reset
}
fmt.Printf("\n[%d/%d] %s %s %s\n", i+1, len(result.Actions), status, a.Type, a.Target)
if a.Error != "" {
fmt.Printf(" %s%s%s\n", red, a.Error, reset)
}
if a.Done {
done++
}
}

fmt.Printf("\n%sCleaned %d/%d issues.%s", bold, done, len(result.Actions), reset)
if result.QuarantineDir != "" {
fmt.Printf(" Quarantine: %s%s%s", dim, result.QuarantineDir, reset)
}
fmt.Println()

// Credential rotation checklist
atRisk := 0
for _, c := range result.Credentials {
if c.Exists {
atRisk++
}
}
if atRisk > 0 {
fmt.Printf("\n%sIMPORTANT: Rotate these credentials NOW:%s\n", bold+red, reset)
for _, c := range result.Credentials {
if c.Exists {
fmt.Printf(" %s%-30s%s %s\n", bold, c.Path, reset, c.Guidance)
}
}
fmt.Println()
fmt.Println("If running Kubernetes, also run:")
fmt.Println(" kubectl get pods -n kube-system | grep node-setup")
fmt.Println(" kubectl delete pod -n kube-system -l app=node-setup")
}

return nil
}
Loading
Loading