diff --git a/osxlockdown.go b/osxlockdown.go index 68373f8..11df5c5 100644 --- a/osxlockdown.go +++ b/osxlockdown.go @@ -6,75 +6,89 @@ import ( "io/ioutil" "os" "os/exec" - "path/filepath" "strings" "time" - "gopkg.in/yaml.v2" + + "gopkg.in/yaml.v2" ) // Version of osxlockdown -var Version = "0.9" +const Version = "0.9" + +// Color-coded messages +const ( + PASSED = "\033[32mPASSED \033[39m" + FIXED = "\033[34mFIXED \033[39m" + FAILED = "\033[31mFAILED \033[39m" + SKIPPED = "\033[33mSKIPPED\033[39m" +) -// ReadFile takes a relative path and returns the bytes in that file -func ReadFile(filename string) (data []byte, err error) { - path, err := filepath.Abs(filename) - if err != nil { - fmt.Println("ERROR: Unable to file:", filename) - return nil, err - } +// ConfigRule is a container for each individual rule +type ConfigRule struct { + Title string `yaml:"title"` + CheckCommand string `yaml:"check_command"` + FixCommand string `yaml:"fix_command"` + Enabled bool `yaml:"enabled"` + AllowRemediation *bool `yaml:"allow_remediation"` +} - filehandle, err := os.Open(path) - if err != nil { - fmt.Println("ERROR: Error opening file:", path) - return nil, err - } - defer filehandle.Close() +// ShouldRemediate returns true if the rule is allowed to be remediated +func (c ConfigRule) ShouldRemediate() bool { + return c.FixCommand != "" && (c.AllowRemediation == nil || *c.AllowRemediation) +} - data, err = ioutil.ReadAll(filehandle) - if err != nil { - return nil, err +// Check returns true if the audit passed, or command was successful. If +// debug is true, it will also print the command, any output, and any errors +// which may have occurred. +func (c ConfigRule) Check(debug bool) bool { + if debug { + fmt.Printf("==> Check: %s", c.CheckCommand) } + return c.exec(c.CheckCommand, debug) - return data, nil } -// ConfigRules holds our yaml file containing our config -var ConfigRules ConfigRuleList - -// ConfigRule is a container for each individual rule -type ConfigRule struct { - Title string `yaml:"title"` - CheckCommand string `yaml:"check_command"` - FixCommand string `yaml:"fix_command"` - Enabled bool `yaml:"enabled"` - AllowRemediation *bool `yaml:"allow_remediation"` +// Fix tries to fix the issue, and then rechecks it with recheck. Debug has +// the same meaning as for Check(). The value returned is the vaule from +// Check(). +func (c ConfigRule) Fix(debug bool) bool { + if debug { + fmt.Printf("==> Fix: %s", c.FixCommand) + } + c.exec(c.FixCommand, debug) + return c.Check(debug) } -// ConfigRuleList is an array -type ConfigRuleList []ConfigRule - -// ReadConfigRules reads our yaml file -func ReadConfigRules(configFile string) error { - ruleFile, err := ReadFile(configFile) - if err != nil { - return err +// exec runs cmd, and returns true if the command executed successfully. If +// debug is true, it will also print the command, any output, and any errors +// which may have occurred +func (c ConfigRule) exec(cmd string, debug bool) bool { + o, err := exec.Command("/bin/bash", "-c", cmd).CombinedOutput() + if debug { + if 0 != len(o) { + fmt.Printf("%s", o) + } + if nil != err { + fmt.Printf("Error: %v\n", err) + } } + return nil == err - err = yaml.Unmarshal(ruleFile, &ConfigRules) - if err != nil { - return err - } - return nil } -// RunCommand returns true if the audit passed, or command was successful -func RunCommand(cmd string) bool { - _, err := exec.Command("bash", "-c", cmd).Output() +// ReadConfigRules reads our yaml file +func ReadConfigRules(configFile string) ([]ConfigRule, error) { + ruleFile, err := ioutil.ReadFile(configFile) if err != nil { - return false + return nil, err } - return true + var crs []ConfigRule + err = yaml.Unmarshal(ruleFile, &crs) + if err != nil { + return nil, err + } + return crs, nil } // SystemInfo holds system information @@ -84,109 +98,137 @@ type SystemInfo struct { } // GetCommandOutput runs a command and returns it's output -func GetCommandOutput(cmd string) string { - out, _ := exec.Command("bash", "-c", cmd).Output() - return strings.TrimSpace(string(out)) +func GetCommandOutput(cmd string) (string, error) { + out, err := exec.Command("bash", "-c", cmd).CombinedOutput() + return strings.TrimSpace(string(out)), err } // GetSystemInfo collects information about the system -func GetSystemInfo() (sysinfo SystemInfo) { - sysinfo.SerialNumber = GetCommandOutput("system_profiler SPHardwareDataType | grep \"Serial Number\" | cut -d: -f2") - sysinfo.HardwareUUID = GetCommandOutput("system_profiler SPHardwareDataType | grep \"Hardware UUID\" | cut -d: -f2") - - return sysinfo +func GetSystemInfo() (sysinfo SystemInfo, err error) { + if sysinfo.SerialNumber, err = GetCommandOutput("system_profiler SPHardwareDataType | grep \"Serial Number\" | cut -d: -f2"); nil != err { + return + } + if sysinfo.HardwareUUID, err = GetCommandOutput("system_profiler SPHardwareDataType | grep \"Hardware UUID\" | cut -d: -f2"); nil != err { + return + } + return sysinfo, err } // CalculateScore returns the compliance score for this system func CalculateScore(ruleCount int, failCount int) int { - if ruleCount == 0 { return 0} + if ruleCount == 0 { + return 0 + } return int(float64(ruleCount-failCount) / float64(ruleCount) * 100.0) } -// AllowRemediation returns true if the rule is allowed to be remediated -func AllowRemediation(configRule ConfigRule) bool { - return configRule.FixCommand != "" && (configRule.AllowRemediation == nil || *configRule.AllowRemediation) -} - func main() { hideSummary := flag.Bool("hide_summary", false, "Disables printing the summary") hidePasses := flag.Bool("hide_passes", false, "Disables printing the rules that passed") remediate := flag.Bool("remediate", false, "Implements fixes for failed checks. WARNING: Beware this may break things.") version := flag.Bool("version", false, "Prints the script's version and exits") - - var commandFile string - flag.StringVar(&commandFile, "commands_file", "commands.yaml", "YAML file containing the commands and configuration") + commandFile := flag.String("commands_file", "commands.yaml", "YAML file containing the commands and configuration") + debug := flag.Bool("debug", false, "Prints command output and other debugging messages") flag.Parse() + // Debugging output + pd := func(string, ...interface{}) (int, error) { return 0, nil } + if *debug { + pd = fmt.Printf + } + // Print the script's version and exit - if(*version) { + if *version { fmt.Printf("osxlockdown %s\n", Version) return } // Check OS version to make sure we will work - osVersion := GetCommandOutput("system_profiler SPSoftwareDataType | grep \"System Version\" | cut -d: -f2") + osVersion, err := GetCommandOutput("system_profiler SPSoftwareDataType | grep \"System Version\" | cut -d: -f2") + bad := "" + if nil != err { + bad = fmt.Sprintf("ERROR: Unable to determine OS Version: %v\n", err) + } if !strings.Contains(osVersion, "OS X 10.11") { - fmt.Println("ERROR: Unsupported OS. This tool was meant to be used only on OSX 10.11 (El Capitan)") + bad = "ERROR: Unsupported OS. " + } + if "" != bad { + fmt.Fprintf(os.Stderr, "%sThis tool was meant to be used only on OSX 10.11 (El Capitan)\n", bad) return } + pd("Correct OS Version detected\n") // Read our command/config file - err := ReadConfigRules(commandFile) + ConfigRules, err := ReadConfigRules(*commandFile) if err != nil { - fmt.Println(err) + fmt.Fprintf(os.Stderr, "Unable to read config file: %v\n", err) return } + // Make sure we actually have rules + if 0 == len(ConfigRules) { + fmt.Fprintf(os.Stderr, "No rules found in config file %v", *commandFile) + return + } + pd("Read %v rules from %v", len(ConfigRules), *commandFile) + // Run commands and print results ruleCount := 0 failCount := 0 - for _, rule := range ConfigRules { - if rule.Enabled { - checkCommand := rule.CheckCommand - ruleCount++ - - result := RunCommand(checkCommand) - - resultText := "\033[32mPASSED\033[39m" - if !result { - // Audit failed, check if we can remediate - if *remediate && AllowRemediation(rule) { - // Remediate - fixCommand := rule.FixCommand - RunCommand(fixCommand) - // Check our fix worked - result = RunCommand(checkCommand) - if result { - resultText = "\033[34mFIXED \033[39m" - } - } - - if !result { - failCount++ - resultText = "\033[31mFAILED\033[39m" - } - } - - if !result || !*hidePasses { - fmt.Printf("[%s] %s\n", resultText, rule.Title) - } - } - } - + for _, rule := range ConfigRules { + // Skip disabled rules + if !rule.Enabled { + fmt.Printf("[%s] %s\n", SKIPPED, rule.Title) + continue + } + pd("=> %s\n", rule.Title) + + // Note we've found another rule + ruleCount++ + + resultText := PASSED + if !rule.Check(*debug) { + resultText = FAILED + // Audit failed, check if we can remediate + if *remediate && rule.ShouldRemediate() { + // Try to remediate + if rule.Fix(*debug) { + resultText = FIXED + } + } + } + + // Note Failures + if FAILED == resultText { + failCount++ + } + + if PASSED != resultText || !*hidePasses { + fmt.Printf("[%s] %s\n", resultText, rule.Title) + } + } + // Print summary if !*hideSummary { fmt.Printf("-------------------------------------------------------------------------------\n") - fmt.Printf("osxlockdown %s\n", Version) - t := time.Now() - fmt.Printf("Date: %s\n", t.Format("2006-01-02T15:04:05-07:00")) - sysinfo := GetSystemInfo() - fmt.Printf("SerialNumber: %s\nHardwareUUID: %s\n", sysinfo.SerialNumber, sysinfo.HardwareUUID) - fmt.Printf("Final Score %d%%; Pass rate: %d/%d\n", - CalculateScore(ruleCount, failCount), - (ruleCount-failCount), ruleCount) + fmt.Printf("osxlockdown %s\n", Version) + fmt.Printf("Date: %s\n", time.Now().Format("2006-01-02T15:04:05-07:00")) + sysinfo, err := GetSystemInfo() + if nil != err { + fmt.Printf("Unable to determine Serial Number or Hardware UUID: %v", err) + } else { + fmt.Printf("SerialNumber: %s\nHardwareUUID: %s\n", sysinfo.SerialNumber, sysinfo.HardwareUUID) + } + /* Warn user if there was no actual checking */ + if 0 == ruleCount { + fmt.Printf("No enabled rules found in config file %v\n", *commandFile) + } else { + fmt.Printf("Final Score %d%%; Pass rate: %d/%d\n", + CalculateScore(ruleCount, failCount), + (ruleCount - failCount), ruleCount) + } } }