Skip to content
Closed
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
18 changes: 17 additions & 1 deletion baseline/cmd_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,24 @@ import (
"github.com/spf13/cobra"
)

var baselineComponents = map[string]string{
"services": "services.ps1",
"processes": "processes.ps1",
"autoruns": "autoruns.ps1",
"ad-users": "ad-users.ps1",
"local-users": "local-users.ps1",
"ad-objects": "ad-objects.ps1",
"ports": "ports.ps1",
}

var dcScripts = []string{"services", "processes", "autoruns", "ad-users", "adobjects", "ports"}
var localScripts = []string{"services", "processes", "autoruns", "local-users", "ports"}

var sysinternalsDirectory = `C:\\ProgramData\\landschaft\\sysinternals`

func SetupCommand(cmd *cobra.Command) {
setupServicesCmd(cmd)
setupCompareCmd(cmd)
setupCreateCmd(cmd)
}

func Run(cmd *cobra.Command) {
Expand Down
261 changes: 261 additions & 0 deletions baseline/compare_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
package baseline

import (
"encoding/csv"
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strings"

"github.com/spf13/cobra"
)

func setupCompareCmd(cmd *cobra.Command) {
compareCmd := &cobra.Command{
Use: "compare",
Short: "Compare baselines",
}

compareAllCmd := &cobra.Command{
Use: "all <baselineA> <baselineB>",
Short: "Compare two baseline directories",
Long: "Compare two directories produced by 'baseline create all' and report added/removed/changed entries for each component.",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
compareCSVDirs(args[0], args[1])
},
}
compareCmd.AddCommand(compareAllCmd)

for name := range baselineComponents {
cmdCmp := &cobra.Command{
Use: name + " <baselineA> <baselineB>",
Short: fmt.Sprintf("Compare %s baselines", name),
Long: fmt.Sprintf("Compare the %s.csv files in two baseline directories and report added/removed/changed entries.", name),
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
err := compareCSVFiles(fmt.Sprintf("%s.csv", name), args[0], args[1])
if err != nil {
fmt.Printf("Error comparing %s: %v\n", name, err)
}
},
}

compareCmd.AddCommand(cmdCmp)
}

cmd.AddCommand(compareCmd)
}

func getAllCSVFiles(dir string) ([]string, error) {
dir, err := filepath.Abs(dir)
if err != nil {
return nil, fmt.Errorf("could not get absolute path of %s: %v", dir, err)
}
files := []string{}
dirEntries, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("could not read directory %s: %v", dir, err)
}
for _, entry := range dirEntries {
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".csv") {
files = append(files, entry.Name())
}
}
return files, nil
}

func compareCSVFiles(fileName, dirA, dirB string) error {
fmt.Println()
fmt.Println(strings.Repeat("=", 60))
fmt.Println()

keyCols := keyColumnsForFile(fileName)
pathA := filepath.Join(dirA, fileName)
pathB := filepath.Join(dirB, fileName)
mapA, err := loadCSVWithKey(pathA, keyCols)
if err != nil {
return fmt.Errorf("could not load %s: %v", pathA, err)
}
mapB, err := loadCSVWithKey(pathB, keyCols)
if err != nil {
return fmt.Errorf("could not load %s: %v", pathB, err)
}

fmt.Printf("Comparing %s\n", fileName)

addedKeys, removedKeys, changed := diffMaps(mapA, mapB)

if len(addedKeys) > 0 {
sort.Strings(addedKeys)
fmt.Printf("Added in %s:\n", dirB)
for _, k := range addedKeys {
fmt.Printf("\t%s\n", formatObject(k, mapB[k]))
}
}
if len(removedKeys) > 0 {
sort.Strings(removedKeys)
fmt.Printf("Removed from %s:\n", dirB)
for _, k := range removedKeys {
fmt.Printf("\t%s\n", formatObject(k, mapA[k]))
}
}
if len(changed) > 0 {
fmt.Println("Changed entries:")
for _, c := range changed {
fmt.Printf("\t%s\n", c)
}
}
return nil
}

func compareCSVDirs(dirA, dirB string) {
filesA, err := getAllCSVFiles(dirA)
if err != nil {
fmt.Printf("Error getting CSV files from directory %s: %v\n", dirA, err)
return
}
filesB, err := getAllCSVFiles(dirB)
if err != nil {
fmt.Printf("Error getting CSV files from directory %s: %v\n", dirB, err)
return
}

sharedFiles := []string{}
for _, fA := range filesA {
if slices.Contains(filesB, fA) {
sharedFiles = append(sharedFiles, fA)
}
}

for _, f := range sharedFiles {
err := compareCSVFiles(f, dirA, dirB)
if err != nil {
fmt.Printf("Error comparing file %s: %v\n", f, err)
}
}
}

func keyColumnsForFile(file string) []string {
switch file {
case "ad-objects.csv":
return []string{"DistinguishedName"}
case "autoruns.csv":
return []string{"Location", "Name", "LaunchString"}
case "ports.csv":
return []string{"LocalAddress", "LocalPort"}
case "processes.csv":
return []string{"Name", "Path"}
case "services.csv":
return []string{"Name"}
case "ad-users.csv":
return []string{"SamAccountName"}
case "local-users.csv":
return []string{"Name"}
default:
return nil
}
}

func loadCSVWithKey(path string, keyCols []string) (map[string]map[string]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()

r := csv.NewReader(f)
recs, err := r.ReadAll()
if err != nil {
return nil, err
}
if len(recs) < 1 {
return nil, fmt.Errorf("empty csv: %s", path)
}
headers := recs[0]
out := make(map[string]map[string]string)

buildKey := func(row []string) string {
if len(keyCols) == 0 {
if len(row) > 0 {
return row[0]
}
return ""
}
vals := make([]string, 0, len(keyCols))
for _, kc := range keyCols {
idx := -1
for i, h := range headers {
if h == kc {
idx = i
break
}
}
if idx >= 0 && idx < len(row) {
vals = append(vals, row[idx])
} else {
vals = append(vals, "")
}
}
return strings.Join(vals, " | ")
}

for _, row := range recs[1:] {
if len(row) == 0 {
continue
}
m := make(map[string]string)
for i, cell := range row {
if i < len(headers) {
m[headers[i]] = cell
} else {
m[fmt.Sprintf("col_%d", i)] = cell
}
}
key := buildKey(row)
out[key] = m
}
return out, nil
}

func formatObject(key string, obj map[string]string) string {
if obj == nil {
return key
}
lines := []string{key}
keys := make([]string, 0, len(obj))
for k := range obj {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
lines = append(lines, fmt.Sprintf("\t\t%s: %s", k, obj[k]))
}
return strings.Join(lines, "\n")
}

func diffMaps(a, b map[string]map[string]string) (added []string, removed []string, changed []string) {
for k := range a {
if _, ok := b[k]; !ok {
removed = append(removed, k)
}
}
for k := range b {
if _, ok := a[k]; !ok {
added = append(added, k)
}
}
for k := range a {
if vb, ok := b[k]; ok {
va := a[k]
for hk, hv := range va {
if vb[hk] != hv {
changed = append(changed, fmt.Sprintf("%s: %s changed from '%s' to '%s'", k, hk, hv, vb[hk]))
}
}
}
}
return
}
83 changes: 83 additions & 0 deletions baseline/create_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package baseline

import (
"fmt"
"os/exec"
"path/filepath"
"strings"

"github.com/UT-CTF/landschaft/misc"
"github.com/UT-CTF/landschaft/util"
"github.com/spf13/cobra"
)

func setupCreateCmd(cmd *cobra.Command) {
createCmd := &cobra.Command{
Use: "create",
Short: "Create baselines",
}

createAllCmd := &cobra.Command{
Use: "all <output-dir>",
Short: "Create all baselines into a directory",
Long: "Create all baselines (services, processes, autoruns, users, adobjects, ports) and save CSV files into the provided output directory.",
Args: cobra.ExactArgs(1),
Example: " landschaft baseline create all C:\\baselines",
Run: func(cmd *cobra.Command, args []string) {
out := args[0]
out, _ = filepath.Abs(out)
_ = misc.EnsureSysinternals(sysinternalsDirectory)
componentList := localScripts
if checkIfDomainController() {
componentList = dcScripts
}

for _, component := range componentList {
fmt.Printf("Creating baseline for %s...\n", component)
createSingleBaseline(component, out)
}
},
}
createCmd.AddCommand(createAllCmd)

for name := range baselineComponents {
cmdC := &cobra.Command{
Use: name + " <baseline-dir>",
Short: fmt.Sprintf("Create %s baseline", name),
Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) {
out := args[0]
out, err := filepath.Abs(out)
if err != nil {
fmt.Printf("Error getting absolute path for output directory: %v\n", err)
return
}
createSingleBaseline(name, out)
},
}

createCmd.AddCommand(cmdC)
}

cmd.AddCommand(createCmd)
}

func checkIfDomainController() bool {
cmd := exec.Command("wmic", "computersystem", "get", "domainrole")
output, err := cmd.Output()
if err != nil {
fmt.Printf("Error checking domain role: %v\n", err)
return false
}
outStr := string(output)
return strings.Contains(outStr, "4") || strings.Contains(outStr, "5")
}

func createSingleBaseline(name, baselineDir string) {
if name == "autoruns" {
_ = misc.EnsureSysinternals(sysinternalsDirectory)
util.RunAndRedirectScript(fmt.Sprintf("baseline/%s", baselineComponents[name]), "-BaselinePath", fmt.Sprintf("'%s'", baselineDir), "-SysinternalsPath", fmt.Sprintf("'%s'", sysinternalsDirectory))
} else {
util.RunAndRedirectScript(fmt.Sprintf("baseline/%s", baselineComponents[name]), "-BaselinePath", fmt.Sprintf("'%s'", baselineDir))
}
}
Loading