Skip to content
This repository has been archived by the owner on Apr 11, 2018. It is now read-only.

Add --debug, formatting, rearrange #52

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
264 changes: 153 additions & 111 deletions osxlockdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
}