Skip to content
This repository was archived by the owner on Apr 11, 2018. It is now read-only.
Open
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
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)
}
}
}