diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 23c644ca..b81a9227 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -7,10 +7,10 @@ jobs:
name: Build
runs-on: ubuntu-latest
steps:
- - name: Set up Go 1.16.3
+ - name: Set up Go
uses: actions/setup-go@v2
with:
- go-version: 1.16.3
+ go-version: 1.18
id: go
- name: Check out code into the Go module directory
diff --git a/Makefile b/Makefile
index 33abdb8a..60179478 100644
--- a/Makefile
+++ b/Makefile
@@ -44,6 +44,8 @@ release:
CGO_ENABLED=0 GOOS=linux go build -ldflags '-s -w' -tags phocus -o ./phocus . && \
CGO_ENABLED=0 GOOS=linux go build -ldflags '-s -w' -o ./aeacus . && \
echo "Linux production build successful!" && \
+ mv crypto.go.bak crypto.go && \
+ echo "Restored crypto.go" && \
mkdir aeacus-win32/ && mkdir aeacus-linux/ && \
mv aeacus.exe aeacus-win32/aeacus.exe && \
mv phocus.exe aeacus-win32/phocus.exe && \
diff --git a/README.md b/README.md
index 9b10012c..cff6f158 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,13 @@
-# aeacus [![Go Report Card](https://goreportcard.com/badge/github.com/elysium-suite/aeacus)](https://goreportcard.com/report/github.com/elysium-suite/aeacus) ![build](https://github.com/elysium-suite/aeacus/workflows/Build/badge.svg) ![test](https://github.com/elysium-suite/aeacus/workflows/Test/badge.svg) ![format](https://github.com/elysium-suite/aeacus/workflows/Format/badge.svg)
+# aeacus [![Go Report Card](https://goreportcard.com/badge/github.com/elysium-suite/aeacus)](https://goreportcard.com/report/github.com/elysium-suite/aeacus)
-
+
`aeacus` is a vulnerability scoring engine for Windows and Linux, with an emphasis on simplicity.
+## V2
+
+`aeacus` has recently been updated to version 2.0.0! To view the breaking changes, refer to [./docs/v2.md](./docs/v2.md).
+
## Installation
0. **Extract the release** into `/opt/aeacus` (Linux) or `C:\aeacus\` (Windows).
@@ -14,10 +18,9 @@
- Put your **config** in `/opt/aeacus/scoring.conf` or`C:\aeacus\scoring.conf`.
- - _Don't have a config? See the example at the bottom of this README._
+ - _Don't have a config? See the example below._
- Put your **README data** in `ReadMe.conf`.
- - Use `./aeacus forensics 3` to create three Forensic Question files on the Desktop of the main user.
2. **Check that your config is valid.**
@@ -37,37 +40,69 @@
4. **Prepare the image for release.**
+> **WARNING**: This will remove `scoring.conf`. Back it up somewhere if you want to save it! It will also remove the `aeacus` executable and other sensitive files.
+
```
./aeacus --verbose release
```
-> WARNING: This will remove `scoring.conf`. Back it up somewhere if you want to save it! It will also remove the `aeacus` executable and other sensitive files.
-
## Screenshots
-#### Scoring Report:
+### Scoring Report:
![Scoring Report](./misc/gh/ScoringReport.png)
-#### ReadMe:
+### ReadMe:
![ReadMe](./misc/gh/ReadMe.png)
## Features
- Robust yet simple vulnerability scorer
-- Image deployment (cleanup, README, etc)
+- Image preparation (cleanup, README, etc)
- Remote score reporting
-> Note: `aeacus` ships with very weak crypto on purpose. You need to implement your own crypto functions. See the [Adding Crypto](/docs/crypto.md) for more information.
+> Note: `aeacus` ships with weak crypto on purpose. You should implement your own crypto functions if you want to make it harder to crack. See [Adding Crypto](/docs/crypto.md) for more information.
+
+## Compiling
+
+Only Linux development environments are officially supported. Ubuntu virtual machines work great.
+
+Make sure you have a recent version of `go` installed, as well as `git` and `make`. If you want to compile Windows and Linux, install all dependencies using `go get -v -d -t ./...`. Then to compile, use `go build`, OR make:
+
+- Building for `Linux`: `make lin`
+- Building for `Windows`: `make win`
+
+### Development
+
+If you're developing for `aeacus`, compile with these commands to leave debug symbols in the binaries:
+
+- Building for `Linux`: `make lin-dev`
+- Building for `Windows`: `make win-dev`
+
+### Releases
+
+You can build release files (e.g., `aeacus-linux.zip`). These will have auto-randomized `crypto.go` files.
+
+- Building both platforms: `make release`
-## Checks
+## Documentation
All checks (with examples and notes) [are documented here](docs/checks.md).
+Other documentation:
+- [Non-Check Scoring Configuration](docs/config.md)
+- [Crypto](docs/crypto.md)
+- [Security Model](docs/security.md)
+- [Windows Security Policy](docs/securitypolicy.md)
+
+## Remote Endpoint
+
+Set the `remote` field in the configuration, and your image will use remote scoring. If you want remote scoring, you will need to host a remote scoring endpoint. The authors of this project recommend using [sarpedon](https://github.com/elysium-suite/sarpedon). See [this example remote configuration for Linux aeacus](docs/examples/linux-remote.conf).
+
## Configuration
-The configuration is written in TOML. All fields are optional unless otherwise specified. See the below example:
+The configuration is written in TOML. Here is a minimal example:
```toml
name = "ubuntu-18-supercool" # Image name
@@ -75,103 +110,70 @@ title = "CoolCyberStuff Practice Round" # Round title
os = "Ubuntu 18.04" # OS, used for README
user = "coolUser" # Main user for the image
-# If remote is specified, aeacus will report its score
-# and refuse to score if the remote server does not accept
-# its messages and Team ID (unless "local" is set to "yes")
-# Make sure to include the scheme (http, https...)
-# NOTE: _DON'T_ include a slash after the url!
-remote = "https://192.168.1.100"
-
-# If password is specified, it will be used to
-# encrypt remote reporting traffic
-# NOTE: Server must have the same password set
-password = "HackersArentReal"
-
-# If local is set to true, then the image will give
-# feedback and score regardless of whether or not
-# remote scoring is working
-local = true
-
-# If enddate exists, image will self destruct
-# after the time specified. The format is:
-# YEAR/MO/DA HR:MN:SC ZONE
-enddate = "2020/03/21 15:04:05 PDT"
-
-# If nodestroy is set to true, then the image will not
-# self destruct, only the aeacus folder will be deleted.
-# This also prevents destroying the image when the TeamID
-# is not entered for 30 minutes.
-nodestroy = true
-
-# If disableshell is set to true, the aeacus binary will not
-# reach out for the debug remote shell.
-disableshell = true
-
-# Set the version of this scoring file. This is not a number
-# that is changed for YOUR versions, it is changed in tandem
-# with the current version of aeacus.
-# If you're ever unsure of the version, just run "aeacus version"
-version = "1.8.2"
+# Set the aeacus version of this scoring file. Set this to the version
+# of aeacus you are using. This is used to make sure your configuration,
+# if re-used, is compatible with the version of aeacus being used.
+#
+# You can print your version of aeacus with ./aeacus version.
+version = "2.0.0"
[[check]]
message = "Removed insecure sudoers rule"
points = 10
[[check.pass]]
- type="FileContainsNot"
- arg1="/etc/sudoers"
- arg2="NOPASSWD"
+ type = "FileContainsNot"
+ path = "/etc/sudoers"
+ value = "NOPASSWD"
[[check]]
# If no message is specified, one is auto-generated
points = 20
[[check.pass]]
- type="FileExistsNot"
- arg1="/etc/secrets.zip"
+ type = "FileExistsNot"
+ path = "/usr/bin/ufw-backdoor"
- [[check.pass]] # You can code multiple pass conditions
- type="Command" # they must ALL succeed for the check to pass
- arg1="ufw status"
+ [[check.pass]] # You can code multiple pass conditions, but
+ type = "Command" # they must ALL succeed for the check to pass!
+ cmd = "ufw status"
[[check]]
message = "Malicious user 'user' can't read /etc/shadow"
-# If no points are specified, they are auto-calculated.
-# If total points specified is less than 100, each check
-# is assigned points (integers) that add up to 100.
-# If total points already specified is above 100, each check
-# without points is worth 2 points.
+# If no points are specified, they are auto-calculated out of 100.
[[check.pass]]
- type="CommandNot"
- arg1="sudo -u user cat /etc/shadow"
+ type = "CommandNot"
+ cmd = "sudo -u user cat /etc/shadow"
- [[check.pass]]
- type="FileExists"
- arg1="/etc/shadow"
+ [[check.pass]] # "pass" conditions are logically AND with other pass
+ type = "FileExists" # conditions. This means they all must pass for a check
+ path = "/etc/shadow" # to be considered successful.
[[check.passoverride]] # If you a check to succeed if just one condition
- type="UserExistsNot" # passes, regardless of other pass checks, use
- arg1="user" # an override pass (passoverride). This is still
- # overridden by fail conditions.
+ type = "UserExistsNot" # passes, regardless of other pass checks, use
+ user = "user" # an override pass (passoverride). This is a logical OR.
+ # passoverride is overridden by fail conditions.
- [[check.fail]] # If any fail conditions pass, the whole check
- type="FileExistsNot" # will fail
- arg1="/etc/shadow"
+ [[check.fail]] # If any fail conditions succeed, the entire check will fail.
+ type = "FileExistsNot"
+ path = "/etc/shadow"
[[check]]
message = "Administrator has been removed"
points = -5 # This check is now a penalty, because it has negative points
[[check.pass]]
- type="UserExistsNot"
- arg1="coolAdmin"
+ type = "UserExistsNot"
+ user = "coolAdmin"
```
+See more in-depth examples, including remote reporting, [here](https://github.com/elysium-suite/aeacus/tree/master/docs/examples).
+
## ReadMe Configuration
-Put your README in `ReadMe.conf`. It's pretty self-explanatory. Here's a template:
+Put your README in `ReadMe.conf`. Here's a commented template:
```html
@@ -215,34 +217,23 @@ niceUser
## Information Gathering
-The `aeacus` binary supports gathering information on Windows in cases where it's tough to gather what the scoring system can see.
+The `aeacus` binary supports gathering information (on **Windows** only) in cases where it's tough to gather what the scoring system can see.
-Print information with `./aeacus info type` where `type` is one the following:
+Print information with `./aeacus info type` where `type` is one the following (NOTE: this is deprecated and will be removed in a future release):
### Windows
-- `packages` (shows installed programs)
-
-## Remote Endpoint
-
-The authors of this project recommend using [sarpedon](https://github.com/elysium-suite/sarpedon) as the remote scoring endpoint.
+- `programs` (shows installed programs)
+- `users` (shows local users)
+- `admins` (shows local administrator users)
## Tips and Tricks
- Easily change the branding by replacing `assets/img/logo.png`.
-- On Linux, you can run `./aeacus configure` to launch a GUI tool for configuring vulnerabilities.
-
-## Compiling
-If you need a tool to quickly install `go` and a few other tools, use [this](https://github.com/elysium-suite/aeacus/blob/master/misc/dev/install.sh) to help you out!
-Once you install `go` (make sure you use a recent version) and install dependencies using `go get -v -d -t ./...`, you can build with these commands:
-
-- Building for `Linux`: `make lin`
-- Building for `Windows`: `make win`
-
-### Development compliation
-
-- Building for `Linux`: `make lin-dev`
-- Building for `Windows`: `make win-dev`
+- Test your scoring configuration in a loop:
+``` bash
+while true; do ./aeacus -v; sleep 20; done
+```
## Contributing and Disclaimer
diff --git a/TODO.md b/TODO.md
deleted file mode 100644
index c8af6d7c..00000000
--- a/TODO.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# todo
-
-- remote
- - status/time limit actually enforced
- - see comments in scoring.go and remote.go
-- info
- - other things? esp for windows
-- windows
-
- - improve scoring.conf example crypto (add aes-gcm, obfuscate key, etc)
- - fix windows service quit WaitGroup (phocus_windows.go)
- - ^^ THIS IS LARGELY FIXED. I count null bytes to detect unicode vs ansi. However, when the text read is only one character (for example, `b`), it will fail if unicode
- - binary reg checks
-
-- security
-
- - rsa pub/privkey infra for encrypting scoring config !!! (thanks alvin)
- - disable net/http using HTTP_PROXY environmental variable
- - obfuscate nonencrypted config args to obfuscate types of call
- - right now they're empty
- - add fake reg/file retrives to obfuscate real calls on windwos
- - add fake file retrieves/read to obfuscate calls on linux
- - anti-tracing and anti-debugging
- - delete system if tracing detected
- - use syscall PTRACEME and see if it errors out (or similar)
- - check if any ebpf blobs are loaded into kernel
- - refuse to run if not signed (? how to implement)
-
-- verified vulns on other side (have vuln list on server as well)
-
-- QoL
-
- - spellcheck/typo alert in config
- - if arg number is wrong, alert them (ex. no arg2 when its required)
- - TESTS!!!
- - TESTS!!!
- - TESTS!!!
- - TESTS!!!
- - TESTS!!!
- - TESTS!!!
- - TESTS!!!
- - TESTS!!!
-
-- checks to implement
-
- - windows startup programs
- - windows and linux updates
- - windows (Make less janky)
- - windows service-specific hardening and checks
- - windows DEP
-
-- release
-
- - windows
- - Detect if firefox.exe is in x86 Program Files or just Program Files
- - clear regedit opening
-
-- hard/long term
-
- - verify binary
- - replace shell checks with lower-level more reliable things, winAPI, whatever
-
-- bugs
- Release bug:
- Obscure Check 3 stopped working after release, works with aeacus --verbose score but not post release with phocus
- Profile bug:
- When scoring the image there are certain PowerShell commands run which run without the -noprofile argument and so the profile ends up getting run about 3 times or so.
- Doesn't seem to be caused by the command output checks bc I had 4 of them and the profile was only run 3 times, but I could be wrong
diff --git a/aeacus.go b/aeacus.go
index 0d02c535..7f628ef5 100644
--- a/aeacus.go
+++ b/aeacus.go
@@ -1,14 +1,10 @@
-// +build !phocus
+//go:build !phocus
package main
import (
- "errors"
- "log"
"os"
- "strconv"
- "github.com/elysium-suite/aeacus/cmd"
"github.com/urfave/cli/v2"
)
@@ -21,37 +17,48 @@ import (
//////////////////////////////////////////////////////////////////
func main() {
- cmd.FillConstants()
- cmd.RunningPermsCheck()
app := &cli.App{
UseShortOptionHandling: true,
EnableBashCompletion: true,
Name: "aeacus",
Usage: "setup and score vulnerabilities in an image",
Before: func(c *cli.Context) error {
- cmd.ParseFlags(c)
+ err := determineDirectory()
+ if err != nil {
+ return err
+ }
return nil
},
Action: func(c *cli.Context) error {
- cmd.CheckConfig(cmd.ScoringConf)
- cmd.ScoreImage()
+ permsCheck()
+ readConfig()
+ scoreImage()
return nil
},
Flags: []cli.Flag{
&cli.BoolFlag{
- Name: "verbose",
- Aliases: []string{"v"},
- Usage: "Print extra information",
+ Name: "verbose",
+ Aliases: []string{"v"},
+ Usage: "Print extra information",
+ Destination: &verboseEnabled,
},
&cli.BoolFlag{
- Name: "debug",
- Aliases: []string{"d"},
- Usage: "Print a lot of information",
+ Name: "debug",
+ Aliases: []string{"d"},
+ Usage: "Print a lot of information",
+ Destination: &debugEnabled,
},
&cli.BoolFlag{
- Name: "yes",
- Aliases: []string{"y"},
- Usage: "Automatically answer 'yes' to any prompts",
+ Name: "yes",
+ Aliases: []string{"y"},
+ Usage: "Automatically answer 'yes' to any prompts",
+ Destination: &yesEnabled,
+ },
+ &cli.StringFlag{
+ Name: "dir",
+ Aliases: []string{"r"},
+ Usage: "Directory for aeacus and its files",
+ Destination: &dirPath,
},
},
Commands: []*cli.Command{
@@ -60,8 +67,9 @@ func main() {
Aliases: []string{"s"},
Usage: "Score image with current scoring config",
Action: func(c *cli.Context) error {
- cmd.CheckConfig(cmd.ScoringConf)
- cmd.ScoreImage()
+ permsCheck()
+ readConfig()
+ scoreImage()
return nil
},
},
@@ -70,28 +78,18 @@ func main() {
Aliases: []string{"c"},
Usage: "Check that the scoring config is valid",
Action: func(c *cli.Context) error {
- cmd.CheckConfig(cmd.ScoringConf)
+ readConfig()
return nil
},
},
{
Name: "readme",
Aliases: []string{"rd"},
- Usage: "Compile the readme",
+ Usage: "Compile the README",
Action: func(c *cli.Context) error {
- cmd.CheckConfig(cmd.ScoringConf)
- cmd.GenReadMe()
- return nil
- },
- },
- {
- Name: "test",
- Aliases: []string{"t"},
- Usage: "Score the image and render a readme",
- Action: func(c *cli.Context) error {
- cmd.CheckConfig(cmd.ScoringConf)
- cmd.GenReadMe()
- cmd.ScoreImage()
+ permsCheck()
+ readConfig()
+ genReadMe()
return nil
},
},
@@ -100,40 +98,23 @@ func main() {
Aliases: []string{"e"},
Usage: "Encrypt scoring configuration",
Action: func(c *cli.Context) error {
- cmd.WriteConfig(cmd.ScoringConf, cmd.ScoringData)
+ permsCheck()
+ readConfig()
+ writeConfig()
return nil
},
},
{
Name: "decrypt",
Aliases: []string{"d"},
- Usage: "Check that scoring data file is valid",
+ Usage: "Check that encrypted scoring data file is valid",
Action: func(c *cli.Context) error {
- err := cmd.ReadScoringData()
- return err
- },
- },
- {
- Name: "forensics",
- Aliases: []string{"f"},
- Usage: "Create forensic question files",
- Action: func(c *cli.Context) error {
- numFqs, err := strconv.Atoi(c.Args().First())
- if err != nil {
- return errors.New("Invalid or missing number passed to forensics")
+ permsCheck()
+ err := readScoringData()
+ if err == nil && verboseEnabled {
+ printConfig()
}
- cmd.CheckConfig(cmd.ScoringConf)
- cmd.CreateFQs(numFqs)
- return nil
- },
- },
- {
- Name: "configure",
- Aliases: []string{"g"},
- Usage: "Launch configuration GUI",
- Action: func(c *cli.Context) error {
- cmd.LaunchConfigGui()
- return nil
+ return err
},
},
{
@@ -141,7 +122,7 @@ func main() {
Aliases: []string{"p"},
Usage: "Launch TeamID GUI prompt",
Action: func(c *cli.Context) error {
- cmd.LaunchIDPrompt()
+ launchIDPrompt()
return nil
},
},
@@ -150,8 +131,9 @@ func main() {
Aliases: []string{"i"},
Usage: "Get info about the system",
Action: func(c *cli.Context) error {
- cmd.SetVerbose(true)
- cmd.GetInfo(c.Args().Get(0))
+ permsCheck()
+ verboseEnabled = true
+ getInfo(c.Args().Get(0))
return nil
},
},
@@ -160,8 +142,7 @@ func main() {
Aliases: []string{"v"},
Usage: "Print the current version of aeacus",
Action: func(c *cli.Context) error {
- println("=== aeacus ===")
- println("version " + cmd.AeacusVersion)
+ println("aeacus version " + version)
return nil
},
},
@@ -170,9 +151,8 @@ func main() {
Aliases: []string{"r"},
Usage: "Prepare the image for release",
Action: func(c *cli.Context) error {
- if !cmd.YesEnabled {
- cmd.ConfirmPrint("Are you sure you want to begin the image release process?")
- }
+ permsCheck()
+ confirm("Are you sure you want to begin the image release process?")
releaseImage()
return nil
},
@@ -182,7 +162,7 @@ func main() {
err := app.Run(os.Args)
if err != nil {
- log.Fatal(err)
+ fail(err.Error())
}
}
@@ -190,12 +170,13 @@ func main() {
// writing the ReadMe/Desktop Files, installing the system service,
// and cleaning the image for release.
func releaseImage() {
- cmd.CheckConfig(cmd.ScoringConf)
- cmd.WriteConfig(cmd.ScoringConf, cmd.ScoringData)
- cmd.GenReadMe()
- cmd.WriteDesktopFiles()
- cmd.ConfigureAutologin()
- cmd.InstallFont()
- cmd.InstallService()
- cmd.CleanUp()
+ readConfig()
+ writeConfig()
+ genReadMe()
+ writeDesktopFiles()
+ configureAutologin()
+ installFont()
+ installService()
+ confirm("Everything is done except cleanup. Are you sure you want to continue, and remove your scoring configuration and other aeacus files?")
+ cleanUp()
}
diff --git a/assets/scripts/stop_scoring.sh b/assets/scripts/stop_scoring.sh
index 1cfcd7c3..2ff34822 100644
--- a/assets/scripts/stop_scoring.sh
+++ b/assets/scripts/stop_scoring.sh
@@ -3,11 +3,11 @@
if zenity --question \
--text="Would you like to stop scoring for this image?" \
--title="Aeacus SE"; then
- notify-send -i /opt/aeacus/assets/img/logo.png "Stopping scoring, and shutting down."
+ notify-send -i /opt/aeacus/assets/img/logo.png "Aeacus SE" "Stopping scoring, and shutting down."
service CSSClient stop
pkill -9 phocus
rm -f /opt/aeacus/phocus /opt/aeacus/scoring.dat
shutdown now
else
- notify-send -i /opt/aeacus/assets/img/logo.png "Confirmation failed!"
+ notify-send -i /opt/aeacus/assets/img/logo.png "Aeacus SE" "Confirmation failed!"
fi
diff --git a/checks.go b/checks.go
new file mode 100644
index 00000000..bf4bba58
--- /dev/null
+++ b/checks.go
@@ -0,0 +1,252 @@
+// checks.go contains checks that are identical for both Linux and Windows.
+
+package main
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "reflect"
+ "regexp"
+ "strings"
+)
+
+// check is the smallest unit that can show up on a scoring report. It holds all
+// the conditions for a check, and its message and points (autogenerated or
+// otherwise).
+type check struct {
+ Message string
+ Points int
+ Fail []cond
+ Pass []cond
+ PassOverride []cond
+}
+
+// cond, or condition, is the parameters for a given test within a check.
+type cond struct {
+ Type string
+ Path string
+ Cmd string
+ User string
+ Group string
+ Name string
+ Key string
+ Value string
+ After string
+}
+
+func (c cond) requireArgs(args ...interface{}) {
+ // Don't process internal calls -- assume the developers know what they're
+ // doing. This also prevents extra errors being printed when they don't pass
+ // required arguments.
+ if c.Type == "" {
+ return
+ }
+
+ v := reflect.ValueOf(c)
+ vType := v.Type()
+ for i := 0; i < v.NumField(); i++ {
+ if vType.Field(i).Name == "Type" {
+ continue
+ }
+
+ required := false
+ for _, a := range args {
+ if vType.Field(i).Name == a {
+ required = true
+ break
+ }
+ }
+
+ if required {
+ if v.Field(i).String() == "" {
+ fail(c.Type+":", "missing required argument '"+vType.Field(i).Name+"'")
+ }
+ } else if v.Field(i).String() != "" {
+ warn(c.Type+":", "specifying unnecessary argument '"+vType.Field(i).Name+"'")
+ }
+
+ }
+}
+
+func (c cond) String() string {
+ output := ""
+ v := reflect.ValueOf(c)
+ typeOfS := v.Type()
+
+ for i := 0; i < v.NumField(); i++ {
+ if v.Field(i).String() == "" {
+ continue
+ }
+ output += fmt.Sprintf("\t%s: %v\n", typeOfS.Field(i).Name, v.Field(i).String())
+ }
+ return output
+}
+
+func handleReflectPanic(condFunc string) {
+ if r := recover(); r != nil {
+ fail("Check type does not exist: "+condFunc, "("+r.(*reflect.ValueError).Error()+")")
+ }
+}
+
+// runCheck executes a single condition check.
+func runCheck(cond cond) bool {
+ if err := deobfuscateCond(&cond); err != nil {
+ fail(err.Error())
+ }
+ defer obfuscateCond(&cond)
+ debug("Running condition:\n", cond)
+
+ not := "Not"
+ condFunc := ""
+ negation := false
+ condEnding := cond.Type[len(cond.Type)-len(not) : len(cond.Type)]
+ if condEnding == not {
+ negation = true
+ condFunc = cond.Type[:len(cond.Type)-len(not)]
+ } else {
+ condFunc = cond.Type
+ }
+
+ // Catch panic if check type doesn't exist
+ defer handleReflectPanic(condFunc)
+
+ // Using reflection to find the correct function to call.
+ vals := reflect.ValueOf(cond).MethodByName(condFunc).Call([]reflect.Value{})
+ result := vals[0].Bool()
+ err := vals[1]
+
+ if negation {
+ debug("Result is", !result, "(was", result, "before negation) and error is", err)
+ return err.IsNil() && !result
+ }
+
+ debug("Result is", result, "and error is", err)
+ return err.IsNil() && result
+}
+
+// CommandContains checks if a given shell command contains a certain output.
+// This check will always fail if the command returns an error.
+func (c cond) CommandContains() (bool, error) {
+ c.requireArgs("Cmd", "Value")
+ out, err := shellCommandOutput(c.Cmd)
+ return strings.Contains(strings.TrimSpace(out), c.Value), err
+}
+
+// CommandOutput checks if a given shell command produces an exact output. This
+// check will always fail if the command returns an error.
+func (c cond) CommandOutput() (bool, error) {
+ c.requireArgs("Cmd", "Value")
+ out, err := shellCommandOutput(c.Cmd)
+ return strings.TrimSpace(out) == c.Value, err
+}
+
+// DirContains returns true if any file in the directory matches the regular
+// expression provided.
+func (c cond) DirContains() (bool, error) {
+ c.requireArgs("Path", "Value")
+ result, err := cond{
+ Path: c.Path,
+ }.PathExists()
+ if err != nil {
+ return false, err
+ }
+ if !result {
+ return false, errors.New("path does not exist")
+ }
+
+ var files []string
+ err = filepath.Walk(c.Path, func(path string, info os.FileInfo, err error) error {
+ if !info.IsDir() {
+ files = append(files, path)
+ }
+ if len(files) > 10000 {
+ fail("Recursive indexing has exceeded limit, erroring out.")
+ return errors.New("Indexed too many files in recursive search")
+ }
+ return nil
+ })
+
+ if err != nil {
+ return false, err
+ }
+
+ for _, file := range files {
+ c.Path = file
+ result, err := c.FileContains()
+ if os.IsPermission(err) {
+ return false, err
+ }
+ if result {
+ return result, nil
+ }
+ }
+ return false, nil
+}
+
+// DirContainsRegex is an alias for DirContains
+func (c cond) DirContainsRegex() (bool, error) {
+ return c.DirContains()
+}
+
+// FileContains determines whether a file contains a given regular expression.
+//
+// Newlines in regex may not work as expected, especially on Windows. It's
+// best to not use these (ex. ^ and $).
+func (c cond) FileContains() (bool, error) {
+ c.requireArgs("Path", "Value")
+ fileContent, err := readFile(c.Path)
+ if err != nil {
+ return false, err
+ }
+ found := false
+ for _, line := range strings.Split(fileContent, "\n") {
+ found, err = regexp.Match(c.Value, []byte(line))
+ if err != nil {
+ fail("There's an error with your regular expression for FileContains: " + err.Error())
+ return false, err
+ }
+ if found {
+ break
+ }
+ }
+ return found, err
+}
+
+// FileContainsRegex is an alias for FileContains
+func (c cond) FileContainsRegex() (bool, error) {
+ return c.FileContains()
+}
+
+// FileEquals calculates the SHA256 sum of a file and compares it with the hash
+// provided in the check.
+func (c cond) FileEquals() (bool, error) {
+ c.requireArgs("Path", "Value")
+ fileContent, err := readFile(c.Path)
+ if err != nil {
+ return false, err
+ }
+ hasher := sha256.New()
+ _, err = hasher.Write([]byte(fileContent))
+ if err != nil {
+ return false, err
+ }
+ hash := hex.EncodeToString(hasher.Sum(nil))
+ return hash == c.Value, nil
+}
+
+// PathExists is a wrapper around os.Stat and os.IsNotExist, and determines
+// whether a file or folder exists.
+func (c cond) PathExists() (bool, error) {
+ c.requireArgs("Path")
+ _, err := os.Stat(c.Path)
+ if err != nil && os.IsNotExist(err) {
+ return false, nil
+ } else if err != nil {
+ return false, err
+ }
+ return true, nil
+}
diff --git a/checks_linux.go b/checks_linux.go
new file mode 100644
index 00000000..4ca32906
--- /dev/null
+++ b/checks_linux.go
@@ -0,0 +1,153 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+ "syscall"
+)
+
+func (c cond) AutoCheckUpdatesEnabled() (bool, error) {
+ return cond{
+ Path: "/etc/apt/apt.conf.d/",
+ Value: `(?i)^\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*$`,
+ }.DirContains()
+}
+
+// Command checks if a given shell command ran successfully (that is, did not
+// return or raise any errors).
+func (c cond) Command() (bool, error) {
+ c.requireArgs("Cmd")
+ if c.Cmd == "" {
+ fail("Missing command for", c.Type)
+ }
+ err := shellCommand(c.Cmd)
+ if err != nil {
+ return false, nil
+ }
+ return true, nil
+}
+
+func (c cond) FirewallUp() (bool, error) {
+ return cond{
+ Path: "/etc/ufw/ufw.conf",
+ Value: `^\s*ENABLED=yes\s*$`,
+ }.FileContains()
+}
+
+func (c cond) GuestDisabledLDM() (bool, error) {
+ guestStr := `\s*allow-guest\s*=\s*false`
+ result, err := cond{
+ Path: "/usr/share/lightdm/lightdm.conf.d/",
+ Value: guestStr,
+ }.DirContains()
+ if !result {
+ return cond{
+ Path: "/etc/lightdm/",
+ Value: guestStr,
+ }.DirContains()
+ }
+ return result, err
+}
+
+func (c cond) KernelVersion() (bool, error) {
+ c.requireArgs("Value")
+ utsname := syscall.Utsname{}
+ err := syscall.Uname(&utsname)
+ releaseUint := []byte{}
+ for i := 0; i < 65; i++ {
+ if utsname.Release[i] == 0 {
+ break
+ }
+ releaseUint = append(releaseUint, uint8(utsname.Release[i]))
+ }
+ return string(releaseUint) == c.Value, err
+}
+
+func (c cond) PasswordChanged() (bool, error) {
+ c.requireArgs("User", "Value")
+ fileContent, err := readFile("/etc/shadow")
+ if err != nil {
+ return false, err
+ }
+ for _, line := range strings.Split(fileContent, "\n") {
+ if strings.Contains(line, c.User+":") {
+ if strings.Contains(line, c.User+":"+c.Value) {
+ return false, nil
+ }
+ return true, nil
+ }
+ }
+ return false, errors.New("user not found")
+}
+
+func (c cond) PermissionIs() (bool, error) {
+ c.requireArgs("Path", "Value")
+ f, err := os.Stat(c.Path)
+ if err != nil {
+ return false, err
+ }
+
+ perm := fmt.Sprint(f.Mode().Perm())
+ if len(perm) != 10 {
+ fail("System permission string is wrong length:", perm)
+ return false, errors.New("Invalid system permission string")
+ }
+
+ c.Value = strings.TrimSpace(c.Value)
+ if len(c.Value) == 9 {
+ c.Value = "-" + c.Value
+ } else if len(c.Value) != 10 {
+ fail("Your permission string is the wrong length (should be 9 or 10 characters):", c.Value)
+ return false, errors.New("Invalid user permission string")
+ }
+
+ for i := 0; i < len(c.Value); i++ {
+ if c.Value[i] == '?' {
+ continue
+ }
+ if c.Value[i] != perm[i] {
+ return false, nil
+ }
+ }
+ return true, nil
+}
+
+func (c cond) ProgramInstalled() (bool, error) {
+ c.requireArgs("Name")
+ return cond{
+ Cmd: "dpkg -s " + c.Name,
+ }.Command()
+}
+
+func (c cond) ProgramVersion() (bool, error) {
+ c.requireArgs("Name", "Value")
+ return cond{
+ Cmd: `dpkg -s ` + c.Name + ` | grep Version | cut -d" " -f2`,
+ }.CommandOutput()
+}
+
+func (c cond) ServiceUp() (bool, error) {
+ // TODO: detect and use other init systems
+ c.requireArgs("Name")
+ return cond{
+ Cmd: "systemctl is-active " + c.Name,
+ }.Command()
+}
+
+func (c cond) UserExists() (bool, error) {
+ c.requireArgs("User")
+ return cond{
+ Path: "/etc/passwd",
+ Value: "^" + c.User + ":",
+ }.FileContains()
+}
+
+func (c cond) UserInGroup() (bool, error) {
+ c.requireArgs("User", "Group")
+ return cond{
+ Path: "/etc/group",
+ Value: c.Group + `[0-9a-zA-Z,:\s+]+` + c.User,
+ }.FileContains()
+}
diff --git a/checks_test.go b/checks_test.go
new file mode 100644
index 00000000..d8ce2032
--- /dev/null
+++ b/checks_test.go
@@ -0,0 +1,159 @@
+// checks_test.go is responsible for testing all non-platform dependent checks.
+package main
+
+import (
+ "testing"
+)
+
+func TestCommandContains(t *testing.T) {
+ c := cond{
+ Cmd: "echo 'hello, world!'",
+ Value: "hello, world!",
+ }
+
+ // Should pass: exact match
+ out, err := c.CommandContains()
+ if err != nil || out != true {
+ t.Error(c, "failed:", out, err)
+ }
+
+ // Should pass: substring
+ c.Value = "hello"
+ out, err = c.CommandContains()
+ if err != nil || out != true {
+ t.Error(c, "failed:", out, err)
+ }
+
+ // Should fail: not substring
+ c.Value = "bye"
+ out, err = c.CommandContains()
+ if err != nil || out != false {
+ t.Error(c, "failed:", out, err)
+ }
+
+ // Should fail: command execution fails
+ c.Value = ""
+ c.Cmd = "commanddoesntexist"
+ out, err = c.CommandContains()
+ if err == nil || out != true {
+ t.Error(c, "failed:", out, err)
+ }
+
+ // Should fail: command returns error
+ c.Cmd = "cat /etc/file/doesnt/exist"
+ out, err = c.CommandContains()
+ if err == nil || out != true {
+ t.Error(c, "failed:", out, err)
+ }
+}
+
+func TestCommandOutput(t *testing.T) {
+ c := cond{
+ Cmd: "echo 'hello, world!'",
+ Value: "hello, world!",
+ }
+
+ // Should pass: exact match
+ out, err := c.CommandOutput()
+ if err != nil || out != true {
+ t.Error(c, "failed:", out, err)
+ }
+
+ // Should fail: just substring
+ c.Value = "hello"
+ out, err = c.CommandOutput()
+ if err != nil || out != false {
+ t.Error(c, "failed:", out, err)
+ }
+
+ // Should fail: not exact or substring
+ c.Value = "bye"
+ out, err = c.CommandOutput()
+ if err != nil || out != false {
+ t.Error(c, "failed:", out, err)
+ }
+
+ // Should fail: command execution fails
+ c.Value = ""
+ c.Cmd = "commanddoesntexist"
+ out, err = c.CommandOutput()
+ if err == nil || out != true {
+ t.Error(c, "failed:", out, err)
+ }
+
+ // Should fail: command returns error
+ c.Cmd = "cat /etc/file/doesnt/exist"
+ out, err = c.CommandOutput()
+ if err == nil || out != true {
+ t.Error(c, "failed:", out, err)
+ }
+}
+
+func TestDirContains(t *testing.T) {
+ c := cond{
+ Path: "misc/tests/dir",
+ Value: "^efgh",
+ }
+ out, err := c.DirContains()
+ if err != nil || out != true {
+ t.Error(c, "failed:", out, err)
+ }
+
+ c.Value = "^efghabcd$"
+ out, err = c.DirContains()
+ if err != nil || out != true {
+ t.Error(c, "failed:", out, err)
+ }
+
+ c.Value = "^aaaaaa$"
+ out, err = c.DirContains()
+ if err != nil || out != false {
+ t.Error(c, "failed:", out, err)
+ }
+
+ c.Value = `spaces\s+in\s+it\s+[0-9]*\s+nums`
+ out, err = c.DirContains()
+ if err != nil || out != true {
+ t.Error(c, "failed:", out, err)
+ }
+
+ c.Value = `spaces\s+in\s+it\s+[1-5]*\s+nums`
+ out, err = c.DirContains()
+ if err != nil || out != false {
+ t.Error(c, "failed:", out, err)
+ }
+
+}
+
+func TestFileContains(t *testing.T) {
+ c := cond{
+ Path: "misc/tests/TestFileContains.txt",
+ Value: "^hello",
+ }
+ out, err := c.FileContains()
+ if err != nil || out != true {
+ t.Error(c, "failed:", out, err)
+ }
+
+ c.Value = "nothere"
+ out, err = c.FileContains()
+ if err != nil || out != false {
+ t.Error(c, "failed:", out, err)
+ }
+}
+
+func TestPathExists(t *testing.T) {
+ c := cond{
+ Path: "misc/tests/",
+ }
+ out, err := c.PathExists()
+ if err != nil || out != true {
+ t.Error(c, "failed:", out, err)
+ }
+
+ c.Path = "misc/doesntexist"
+ out, err = c.PathExists()
+ if err != nil || out != false {
+ t.Error(c, "failed:", out, err)
+ }
+}
diff --git a/checks_test_linux.go b/checks_test_linux.go
new file mode 100644
index 00000000..387768cc
--- /dev/null
+++ b/checks_test_linux.go
@@ -0,0 +1,29 @@
+package main
+
+import "testing"
+
+func TestCommand(t *testing.T) {
+ c := cond{
+ Cmd: "echo 'hello, world!'",
+ }
+
+ // Should pass: command ran
+ out, err := c.Command()
+ if err != nil || out != true {
+ t.Error(c, "failed:", out, err)
+ }
+
+ // Should fail: command execution fails
+ c.Cmd = "commanddoesntexist"
+ out, err = c.Command()
+ if err == nil || out != false {
+ t.Error(c, "failed:", out, err)
+ }
+
+ // Should fail: command returns error
+ c.Cmd = "cat /etc/file/doesnt/exist"
+ out, err = c.Command()
+ if err == nil || out != false {
+ t.Error(c, "failed:", out, err)
+ }
+}
diff --git a/checks_windows.go b/checks_windows.go
new file mode 100644
index 00000000..9f830aff
--- /dev/null
+++ b/checks_windows.go
@@ -0,0 +1,421 @@
+package main
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "golang.org/x/sys/windows/registry"
+
+ wapi "github.com/iamacarpet/go-win64api"
+ wapiShared "github.com/iamacarpet/go-win64api/shared"
+)
+
+func (c cond) BitlockerEnabled() (bool, error) {
+ status, err := wapi.GetBitLockerConversionStatusForDrive("C:")
+ if err == nil {
+ if status.ConversionStatus == wapiShared.FULLY_ENCRYPTED ||
+ status.ConversionStatus == wapiShared.ENCRYPTION_IN_PROGRESS {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func (c cond) FileOwner() (bool, error) {
+ c.requireArgs("Path", "Name")
+ owner, err := shellCommandOutput("(Get-Acl " + c.Path + ").Owner")
+ owner = strings.TrimSpace(owner)
+ return owner == c.Name, err
+}
+
+func (c cond) FirewallUp() (bool, error) {
+ fwProfilesInt := []int{wapi.NET_FW_PROFILE2_DOMAIN, wapi.NET_FW_PROFILE2_PRIVATE, wapi.NET_FW_PROFILE2_PUBLIC}
+ for profile := range fwProfilesInt {
+ profileResult, err := wapi.FirewallIsEnabled(int32(profile))
+ if err != nil {
+ return false, err
+ } else if !profileResult {
+ return false, nil
+ }
+ }
+ return true, nil
+}
+
+// PasswordChanged checks if the password for a given user was changed more
+// recently than specified. The date format output by this command is:
+// Monday, January 02, 2006 3:04:05 PM
+// Which somehow manages to defy every common date format. Thanks, Windows.
+func (c cond) PasswordChanged() (bool, error) {
+ c.requireArgs("User", "After")
+ timeStr := "Monday, January 02, 2006 3:04:05 PM"
+ configDate, err := time.Parse(timeStr, strings.TrimSpace(c.After))
+ if err != nil {
+ return false, err
+ }
+ changed, err := shellCommandOutput(`(Get-LocalUser ` + c.User + `).PasswordLastSet`)
+ if err != nil {
+ return false, err
+ }
+ changeDate, err := time.Parse(timeStr, strings.TrimSpace(changed))
+ if err != nil {
+ return false, err
+ }
+ return changeDate.After(configDate), nil
+}
+
+func (c cond) ProgramInstalled() (bool, error) {
+ c.requireArgs("Name")
+ programList, err := getPrograms()
+ if err != nil {
+ return false, err
+ }
+ for _, p := range programList {
+ if strings.Contains(p, c.Name) {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func (c cond) ProgramVersion() (bool, error) {
+ c.requireArgs("Name", "Value")
+ prog, err := getProgram(c.Name)
+ if err != nil {
+ return false, err
+ }
+ return prog.DisplayVersion == c.Value, nil
+}
+
+func (c cond) RegistryKey() (bool, error) {
+ c.requireArgs("Key", "Value")
+ registryArgs := regexp.MustCompile(`\\+`).Split(c.Key, -1)
+ if len(registryArgs) < 2 {
+ fail("Invalid key for RegistryKey. Did you supply 'key'?")
+ return false, errors.New("invalid registry key path: " + c.Key)
+ }
+ registryHiveText := registryArgs[0]
+ keyPath := fmt.Sprintf(strings.Join(registryArgs[1:len(registryArgs)-1], `\`))
+ keyLoc := registryArgs[len(registryArgs)-1]
+
+ var registryHive registry.Key
+ switch registryHiveText {
+ case "HKEY_CLASSES_ROOT", "HKCR":
+ registryHive = registry.CLASSES_ROOT
+ case "HKEY_CURRENT_USER", "HKCU":
+ registryHive = registry.CURRENT_USER
+ case "HKEY_LOCAL_MACHINE", "HKLM", "MACHINE":
+ registryHive = registry.LOCAL_MACHINE
+ case "HKEY_USERS", "HKU":
+ registryHive = registry.USERS
+ case "HKEY_CURRENT_CONFIG", "HKCC":
+ registryHive = registry.CURRENT_CONFIG
+ case "SOFTWARE":
+ registryHive = registry.LOCAL_MACHINE
+ keyPath = `SOFTWARE\` + keyPath
+ default:
+ fail("Unknown registry hive: " + registryHiveText)
+ return false, errors.New("Unknown registry hive " + registryHiveText)
+ }
+
+ debug("Getting key", keyPath, "from hive ", registryHiveText)
+ // Actually get the key
+ k, err := registry.OpenKey(registryHive, keyPath, registry.QUERY_VALUE)
+ if err != nil {
+ if verboseEnabled {
+ warn("Registry opening key failed:", err)
+ }
+ return false, err
+ }
+ defer k.Close()
+
+ // Fetch registry value
+ registrySlice := make([]byte, 256)
+ regLength, valType, err := k.GetValue(keyLoc, registrySlice)
+ if err != nil {
+ // Error is probably about the key not existing. This is fine, some keys
+ // are not defined until the setting is explicitly set. However, the
+ // check should not pass for RegistryKey or RegistryKeyNot, so we return
+ // an error.
+ warn("Failed to open registry key:", err)
+ return false, err
+ }
+
+ registrySlice = registrySlice[:regLength]
+ debug("Retrieved registry value was", registrySlice, "length", regLength, "value", valType)
+
+ // Determine value type to convert to string
+ var registryValue string
+ switch valType {
+ case 1: // SZ
+ registryValue, _, err = k.GetStringValue(keyLoc)
+ case 2: // EXPAND_SZ
+ registryValue, _, err = k.GetStringValue(keyLoc)
+ case 3: // BINARY
+ fail("Binary registry format not yet supported.")
+ case 4: // DWORD
+ registryValue = strconv.FormatUint(uint64(binary.LittleEndian.Uint32(registrySlice)), 10)
+ default:
+ fail("Unknown registry type: " + fmt.Sprint(valType))
+ }
+
+ // fmt.Printf("Registry value: %s, keyvalue %s\n", registryValue, keyValue)
+ if registryValue == c.Value {
+ return true, err
+ }
+ return false, err
+}
+
+func (c cond) RegistryKeyExists() (bool, error) {
+ c.requireArgs("Key")
+ _, err := c.RegistryKey()
+ if err != nil {
+ if err == registry.ErrNotExist {
+ return false, nil
+ } else {
+ return false, err
+ }
+ } else {
+ return true, nil
+ }
+}
+
+func (c cond) ScheduledTaskExists() (bool, error) {
+ c.requireArgs("Name")
+ return cond{
+ Cmd: "(Get-ScheduledTask -TaskName '" + c.Name + "').TaskName",
+ Value: c.Name,
+ }.CommandOutput()
+}
+
+func (c cond) SecurityPolicy() (bool, error) {
+ c.requireArgs("Key", "Value")
+ var desiredString string
+
+ // If the passed key is one we know is in the registry, just wrap
+ // RegistryKey.
+ if regKey, ok := secpolToKey[c.Key]; ok {
+ return cond{
+ Key: regKey,
+ Value: c.Value,
+ }.RegistryKey()
+ }
+
+ // Otherwise, we're going to grab and parse secedit output :/
+ seceditOutput, err := getSecedit()
+ if err != nil {
+ return false, err
+ }
+
+ re := regexp.MustCompile("(?m)[\r\n]+^.*" + c.Key + ".*$")
+ output := strings.TrimSpace(string(re.Find([]byte(seceditOutput))))
+ if output == "" {
+ return false, errors.New("SecurityPolicy item not found")
+ }
+
+ if c.Key == "NewAdministratorName" || c.Key == "NewGuestName" {
+ // These two are strings, not numbers, so they have ""
+ desiredString = c.Key + " = " + c.Value
+ } else if c.Key == "MinimumPasswordAge" ||
+ c.Key == "MinimumPasswordLength" ||
+ c.Key == "LockoutDuration" ||
+ c.Key == "ResetLockoutCount" ||
+ c.Key == "MaximumPasswordAge" ||
+ c.Key == "LockoutBadCount" ||
+ c.Key == "PasswordHistorySize" {
+
+ // These keys are integers, and support ranges.
+ var outputResult, err = strconv.Atoi(strings.Split(output, " = ")[1])
+ if err != nil {
+ return false, err
+ }
+
+ if strings.Contains(c.Value, "-") {
+ splitVal := strings.Split(c.Value, "-")
+ if len(splitVal) != 2 {
+ fail("Malformed range value:", c.Value)
+ return false, errors.New("invalid c.Value range")
+ }
+ intLow, err := strconv.Atoi(splitVal[0])
+ if err != nil {
+ fail(splitVal[0] + " is not a valid integer for SecurityPolicy check")
+ return false, err
+ }
+ intHigh, err := strconv.Atoi(splitVal[1])
+ if err != nil {
+ fail(splitVal[1] + " is not a valid integer for SecurityPolicy check")
+ return false, err
+ }
+ if intLow <= outputResult && outputResult <= intHigh {
+ return true, nil
+ }
+ } else {
+ desiredValue, err := strconv.Atoi(c.Value)
+ if err != nil {
+ fail(c.Value + " is not a valid integer for SecurityPolicy check")
+ return false, errors.New("invalid c.Value")
+ }
+ if outputResult == desiredValue {
+ return true, nil
+ }
+ }
+ } else {
+ desiredString = c.Key + " = " + c.Value
+ }
+
+ return output == desiredString, nil
+}
+
+func (c cond) ServiceStartup() (bool, error) {
+ c.requireArgs("Name", "Value")
+ var startupNumber string
+ switch c.Value = strings.ToLower(c.Value); c.Value {
+ case "automatic":
+ startupNumber = "2"
+ case "manual":
+ startupNumber = "3"
+ case "disabled":
+ startupNumber = "4"
+ default:
+ fail("Unknown startup type '"+c.Value+"' for", c.Name)
+ return false, errors.New("Unknown status type found for " + c.Name)
+ }
+ serviceKey := `HKLM\SYSTEM\CurrentControlSet\Services\` + c.Name + `\Start`
+ return cond{
+ Key: serviceKey,
+ Value: startupNumber,
+ }.RegistryKey()
+}
+
+func (c cond) ServiceUp() (bool, error) {
+ c.requireArgs("Name")
+ serviceStatus, err := getLocalServiceStatus(c.Name)
+ return serviceStatus.IsRunning, err
+}
+
+func (c cond) ShareExists() (bool, error) {
+ c.requireArgs("Name")
+ return cond{
+ Cmd: "(Get-SmbShare -Name '" + c.Name + "').Name",
+ Value: c.Name,
+ }.CommandOutput()
+}
+
+func (c cond) UserExists() (bool, error) {
+ c.requireArgs("User")
+ user, err := getLocalUser(c.User)
+ if err != nil {
+ return false, err
+ }
+ if user.Username == "" {
+ return false, nil
+ }
+ return true, nil
+}
+
+func (c cond) UserInGroup() (bool, error) {
+ c.requireArgs("User", "Group")
+ users, err := wapi.LocalGroupGetMembers(c.Group)
+ if err != nil {
+ // Error is returned if group is empty.
+ return false, nil
+ }
+ for _, user := range users {
+ justName := strings.Split(user.Name, `\`)[1]
+ if c.User == user.Name || c.User == justName {
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func (c cond) UserDetail() (bool, error) {
+ c.requireArgs("User", "Key", "Value")
+ c.Value = strings.TrimSpace(c.Value)
+ c.Key = strings.TrimSpace(c.Key)
+ lookingFor := false
+ if strings.ToLower(c.Value) == "yes" {
+ lookingFor = true
+ }
+ user, err := getLocalUser(c.User)
+ if err != nil {
+ return false, err
+ }
+ switch c.Key {
+ case "FullName":
+ if user.FullName == c.Value {
+ return true, nil
+ }
+ case "IsEnabled":
+ return user.IsEnabled == lookingFor, nil
+ case "IsLocked":
+ return user.IsLocked == lookingFor, nil
+ case "IsAdmin":
+ return user.IsAdmin == lookingFor, nil
+ case "PasswordNeverExpires":
+ return user.PasswordNeverExpires == lookingFor, nil
+ default:
+ fail("c.Key (" + c.Key + ") passed to userDetail is invalid.")
+ return false, errors.New("invalid detail")
+ }
+ return false, nil
+}
+
+func (c cond) UserRights() (bool, error) {
+ // TODO consider /mergedpolicy when windows domain is active?
+ // domain support is untested, it should be easy to add a domain
+ // flag in the config though. then just make sure you're not getting
+ // invalid local policies instead of gpo
+ c.requireArgs("Name", "Value")
+
+ // TODO: only get section of users -- this can also falsely score correct for other secedit fields (like LegalNoticeText)
+ seceditOutput, err := getSecedit()
+ if err != nil {
+ return false, err
+ }
+
+ re := regexp.MustCompile("(?m)[\r\n]+^.*" + c.Value + ".*$")
+ privilegeString := strings.TrimSpace(string(re.Find([]byte(seceditOutput))))
+ debug("Privilege string for UserRights is:", privilegeString)
+ if privilegeString == "" {
+ return false, nil
+ }
+
+ if strings.Contains(privilegeString, c.Name) {
+ // Sometimes, Windows just puts their user or group name instead of the
+ // SID. Really cool
+ return true, nil
+ }
+
+ privStringSplit := strings.Split(privilegeString, " ")
+ if len(privStringSplit) != 3 {
+ return false, errors.New("error splitting privilege")
+ }
+
+ privStringSplit = strings.Split(privStringSplit[2], ",")
+ for _, sidValue := range privStringSplit {
+ sidValue = strings.TrimSpace(sidValue)
+ userForSid := strings.Split(sidToLocalUser(sidValue[1:]), "\\")
+ userSid := strings.TrimSpace(userForSid[0])
+ if len(userForSid) == 2 {
+ userSid = strings.TrimSpace(userForSid[1])
+ }
+ if userSid == c.Name {
+ return true, nil
+ }
+ }
+
+ return false, err
+}
+
+func (c cond) WindowsFeature() (bool, error) {
+ c.requireArgs("Name")
+ return cond{
+ Cmd: "(Get-WindowsOptionalFeature -Online -FeatureName " + c.Name + ").State",
+ Value: "Enabled",
+ }.CommandOutput()
+}
diff --git a/cmd/checks.go b/cmd/checks.go
deleted file mode 100644
index 2e13083a..00000000
--- a/cmd/checks.go
+++ /dev/null
@@ -1,319 +0,0 @@
-// checks.go contains checks that are identical for both Linux and Windows.
-// If a checkType does not match one specified, it is handed off to
-// processCheck for the OS-specific checks.
-
-package cmd
-
-import (
- "crypto/sha1"
- "encoding/hex"
- "errors"
- "os"
- "os/exec"
- "path/filepath"
- "regexp"
- "strings"
-)
-
-// processCheckWrapper takes the data from a check in the config
-// and runs the correct function with the correct parameters.
-func processCheckWrapper(check *check, checkType, arg1, arg2, arg3 string) bool {
- if err := deobfuscateData(&checkType); err != nil {
- errorPrint(err)
- }
- if err := deobfuscateData(&arg1); err != nil {
- errorPrint(err)
- }
- if err := deobfuscateData(&arg2); err != nil {
- errorPrint(err)
- }
- if err := deobfuscateData(&arg3); err != nil {
- errorPrint(err)
- }
- switch checkType {
- case "Command":
- if check.Message == "" {
- check.Message = "Command \"" + arg1 + "\" passed"
- }
- result, err := command(arg1)
- return err == nil && result
- case "CommandNot":
- if check.Message == "" {
- check.Message = "Command \"" + arg1 + "\" failed"
- }
- result, err := command(arg1)
- return err == nil && !result
- case "CommandOutput":
- if check.Message == "" {
- check.Message = "Command \"" + arg1 + "\" had the output \"" + arg2 + "\""
- }
- result, err := commandOutput(arg1)
- return err == nil && result == arg2
- case "CommandOutputNot":
- if check.Message == "" {
- check.Message = "Command \"" + arg1 + "\" did not have the output \"" + arg2 + "\""
- }
- result, err := commandOutput(arg1)
- return err == nil && result != arg2
- case "CommandContains":
- if check.Message == "" {
- check.Message = "Command \"" + arg1 + "\" contained output \"" + arg2 + "\""
- }
- result, err := commandContains(arg1, arg2)
- return err == nil && result
- case "CommandContainsNot":
- if check.Message == "" {
- check.Message = "Command \"" + arg1 + "\" output did not contain \"" + arg2 + "\""
- }
- result, err := commandContains(arg1, arg2)
- return err == nil && !result
- case "PathExists":
- if check.Message == "" {
- check.Message = "File \"" + arg1 + "\" exists"
- }
- result, err := pathExists(arg1)
- return err == nil && result
- case "PathExistsNot":
- if check.Message == "" {
- check.Message = "File \"" + arg1 + "\" does not exist"
- }
- result, err := pathExists(arg1)
- return err == nil && !result
- case "FileContains":
- if check.Message == "" {
- check.Message = "File \"" + arg1 + "\" contains \"" + arg2 + "\""
- }
- result, err := fileContains(arg1, arg2)
- return err == nil && result
- case "FileContainsNot":
- if check.Message == "" {
- check.Message = "File \"" + arg1 + "\" does not contain \"" + arg2 + "\""
- }
- result, err := fileContains(arg1, arg2)
- return err == nil && !result
- case "FileContainsRegex":
- if check.Message == "" {
- check.Message = "File \"" + arg1 + "\" contains expression \"" + arg2 + "\""
- }
- result, err := fileContainsRegex(arg1, arg2)
- return err == nil && result
- case "FileContainsRegexNot":
- if check.Message == "" {
- check.Message = "File \"" + arg1 + "\" does not contain expression \"" + arg2 + "\""
- }
- result, err := fileContainsRegex(arg1, arg2)
- return err == nil && !result
- case "DirContainsRegex":
- if check.Message == "" {
- check.Message = "Directory \"" + arg1 + "\" contains expression \"" + arg2 + "\""
- }
- result, err := dirContainsRegex(arg1, arg2)
- return err == nil && result
- case "DirContainsRegexNot":
- if check.Message == "" {
- check.Message = "Directory \"" + arg1 + "\" does not contain expression \"" + arg2 + "\""
- }
- result, err := dirContainsRegex(arg1, arg2)
- return err == nil && !result
- case "FileEquals":
- if check.Message == "" {
- check.Message = "File \"" + arg1 + "\" matches hash"
- }
- result, err := fileEquals(arg1, arg2)
- return err == nil && result
- case "FileEqualsNot":
- if check.Message == "" {
- check.Message = "File \"" + arg1 + "\" doesn't match hash"
- }
- result, err := fileEquals(arg1, arg2)
- return err == nil && !result
- case "ProgramInstalled":
- if check.Message == "" {
- check.Message = "Program " + arg1 + " is installed"
- }
- result, err := programInstalled(arg1)
- return err == nil && result
- case "ProgramInstalledNot":
- if check.Message == "" {
- check.Message = "Program " + arg1 + " has been removed"
- }
- result, err := programInstalled(arg1)
- return err == nil && !result
- case "ServiceUp":
- if check.Message == "" {
- check.Message = "Service \"" + arg1 + "\" is installed and running"
- }
- result, err := serviceUp(arg1)
- return err == nil && result
- case "ServiceUpNot":
- if check.Message == "" {
- check.Message = "Service " + arg1 + " has been stopped"
- }
- result, err := serviceUp(arg1)
- return err == nil && !result
- case "UserExists":
- if check.Message == "" {
- check.Message = "User " + arg1 + " has been added"
- }
- result, err := userExists(arg1)
- return err == nil && result
- case "UserExistsNot":
- if check.Message == "" {
- check.Message = "User " + arg1 + " has been removed"
- }
- result, err := userExists(arg1)
- return err == nil && !result
- case "UserInGroup":
- if check.Message == "" {
- check.Message = "User " + arg1 + " is in group \"" + arg2 + "\""
- }
- result, err := userInGroup(arg1, arg2)
- return err == nil && result
- case "UserInGroupNot":
- if check.Message == "" {
- check.Message = "User " + arg1 + " removed or is not in group \"" + arg2 + "\""
- }
- result, err := userInGroup(arg1, arg2)
- return err == nil && !result
- case "FirewallUp":
- if check.Message == "" {
- check.Message = "Firewall has been enabled"
- }
- result, err := firewallUp()
- return err == nil && result
- case "FirewallUpNot":
- if check.Message == "" {
- check.Message = "Firewall has been disabled"
- }
- result, err := firewallUp()
- return err == nil && !result
- case "ProgramVersion":
- if check.Message == "" {
- check.Message = arg1 + " has a version equal to " + arg2
- }
- result, err := programVersion(arg1, arg2)
- return err == nil && result
- case "ProgramVersionNot":
- if check.Message == "" {
- check.Message = arg1 + " has a version that is not equal to " + arg2
- }
- result, err := programVersion(arg1, arg2)
- return err == nil && !result
- default:
- return processCheck(check, checkType, arg1, arg2, arg3)
- }
-}
-
-func commandOutput(commandGiven string) (string, error) {
- out, err := rawCmd(commandGiven).Output()
- if err != nil {
- return "", err
- }
- return strings.TrimSpace(string(out)), nil
-}
-
-func commandContains(commandGiven, desiredContains string) (bool, error) {
- out, err := rawCmd(commandGiven).Output()
- if err != nil {
- if _, ok := err.(*exec.ExitError); ok {
- return false, nil
- }
- return false, err
- }
- outString := strings.TrimSpace(string(out))
- if strings.Contains(outString, desiredContains) {
- return true, nil
- }
- return false, nil
-}
-
-// pathExists is a wrapper around os.Stat and os.IsNotExist, and determines
-// whether a file or folder exists.
-func pathExists(pathName string) (bool, error) {
- _, err := os.Stat(pathName)
- return !os.IsNotExist(err), nil // TODO is not not IsNotExist instead of nil
-}
-
-// fileContains searches for a given searchString in the provided fileName.
-func fileContains(fileName, searchString string) (bool, error) {
- fileContent, err := readFile(fileName)
- return strings.Contains(strings.TrimSpace(fileContent), searchString), err
-}
-
-// fillContainsRegex determines whether a file contains a given regular
-// expression.
-//
-// Newlines in regex may not work as expected, especially on Windows. It's
-// best to not use these (ex. ^ and $).
-func fileContainsRegex(fileName, expressionString string) (bool, error) {
- fileContent, err := readFile(fileName)
- if err != nil {
- return false, err
- }
- matched := false
- found := false
- for _, line := range strings.Split(fileContent, "\n") {
- found, err = regexp.Match(expressionString, []byte(line))
- if found {
- matched = found
- }
- }
- if err != nil {
- failPrint("There's an error with your regular expression for fileContainsRegex: " + err.Error())
- }
- return matched, err
-}
-
-// dirContainsRegex returns true if any file in the directory matches the regular expression provided
-func dirContainsRegex(dirName, expressionString string) (bool, error) {
- result, err := pathExists(dirName)
- if err != nil || !result {
- return false, errors.New("DirContainsRegex: file does not exist")
- }
-
- var files []string
- err = filepath.Walk(dirName, func(path string, info os.FileInfo, err error) error {
- if !info.IsDir() {
- files = append(files, path)
- }
-
- if len(files) > 10000 {
- failPrint("Recursive indexing has exceeded limit, erroring out.")
- return errors.New("Indexed too many files in recursive search")
- }
-
- return nil
- })
-
- if err != nil {
- return false, err
- }
-
- for _, file := range files {
- result, err := fileContainsRegex(file, expressionString)
- if os.IsPermission(err) {
- return false, err
- }
-
- if result {
- return result, nil
- }
- }
- return false, nil
-}
-
-// fileEquals calculates the SHA1 sum of a file and compares it
-// with the hash provided in the check.
-func fileEquals(fileName, fileHash string) (bool, error) {
- fileContent, err := readFile(fileName)
- if err != nil {
- return false, err
- }
- hasher := sha1.New()
- _, err = hasher.Write([]byte(fileContent))
- if err != nil {
- return false, err
- }
- hash := hex.EncodeToString(hasher.Sum(nil))
- return hash == fileHash, nil
-}
diff --git a/cmd/checks_linux.go b/cmd/checks_linux.go
deleted file mode 100644
index c7f49658..00000000
--- a/cmd/checks_linux.go
+++ /dev/null
@@ -1,189 +0,0 @@
-package cmd
-
-import (
- "os/exec"
- "strings"
-)
-
-// processCheck (Linux) will process Linux-specific checks
-// handed to it by the processCheckWrapper function
-func processCheck(check *check, checkType, arg1, arg2, arg3 string) bool {
- switch checkType {
- case "GuestDisabledLDM":
- if check.Message == "" {
- check.Message = "Guest is disabled"
- }
- result, err := guestDisabledLDM()
- return err == nil && result
- case "GuestDisabledLDMNot":
- if check.Message == "" {
- check.Message = "Guest is enabled"
- }
- result, err := guestDisabledLDM()
- return err == nil && !result
- case "PasswordChanged":
- if check.Message == "" {
- check.Message = "Password for " + arg1 + " has been changed"
- }
- result, err := passwordChanged(arg1, arg2)
- return err == nil && result
- case "PasswordChangedNot":
- if check.Message == "" {
- check.Message = "Password for " + arg1 + " has not been changed"
- }
- result, err := passwordChanged(arg1, arg2)
- return err == nil && !result
- case "KernelVersion":
- if check.Message == "" {
- check.Message = "Kernel is version " + arg1
- }
- result, err := kernelVersion(arg1)
- return err == nil && result
- case "KernelVersionNot":
- if check.Message == "" {
- check.Message = "Kernel is not version " + arg1
- }
- result, err := kernelVersion(arg1)
- return err == nil && !result
- case "AutoCheckUpdatesEnabled":
- if check.Message == "" {
- check.Message = "The system automatically checks for updates daily"
- }
- result, err := autoCheckUpdatesEnabled()
- return err == nil && result
- case "AutoCheckUpdatesEnabledNot":
- if check.Message == "" {
- check.Message = "The system does not automatically checks for updates daily"
- }
- result, err := autoCheckUpdatesEnabled()
- return err == nil && !result
- case "PermissionIs":
- if check.Message == "" {
-
- if arg2 == "octal" {
- check.Message = "The octal permissions of " + arg1 + " are " + arg3
- } else if arg2 == "WorldWritable" {
- check.Message = arg1 + " is world writable"
- } else if arg2 == "WorldReadable" {
- check.Message = arg1 + " is world readable"
- } else {
- check.Message = "Permissions of " + arg1 + " are " + arg3
- }
-
- }
- result, err := permissionIs(arg1, arg2, arg3)
- return err == nil && result
- case "PermissionIsNot":
- if check.Message == "" {
-
- if arg2 == "octal" {
- check.Message = "The octal permissions of " + arg1 + " are not " + arg3
- } else if arg2 == "WorldWritable" {
- check.Message = arg1 + " is not world writable"
- } else if arg2 == "WorldReadable" {
- check.Message = arg1 + " is not world readable"
- } else {
- check.Message = "Permissions of " + arg1 + " are not " + arg3
- }
-
- }
- result, err := permissionIs(arg1, arg2, arg3)
- return err == nil && !result
- default:
- failPrint("No check type " + checkType)
- }
- return false
-}
-
-func command(commandGiven string) (bool, error) {
- cmd := rawCmd(commandGiven)
- if err := cmd.Run(); err != nil {
- if _, ok := err.(*exec.ExitError); ok {
- return false, nil
- }
- return false, err
- }
- return true, nil
-}
-
-func commandInterface(progName string, c ...string) (bool, error) {
- cmd := exec.Command(progName, c...)
- if err := cmd.Run(); err != nil {
- if _, ok := err.(*exec.ExitError); ok {
- return false, nil
- }
- return false, err
- }
- return true, nil
-}
-
-func programInstalled(programName string) (bool, error) {
- return commandInterface("/usr/bin/dpkg", "-s", programName)
-}
-
-func serviceUp(serviceName string) (bool, error) {
- // TODO: detect and use other init systems
- ret, err := commandContains("systemctl is-active "+serviceName, "inactive")
- return !ret, err
-}
-
-func userExists(userName string) (bool, error) {
- return fileContains("/etc/passwd", userName+":x:")
-}
-
-func userInGroup(userName, groupName string) (bool, error) {
- return commandContains("groups "+userName, groupName)
-}
-
-func firewallUp() (bool, error) {
- status, err := commandOutput("ufw status")
- return status == "Status: active", err
-}
-
-func passwordChanged(user, hash string) (bool, error) {
- res, err := fileContains("/etc/shadow", hash)
- return !res, err
-}
-
-func guestDisabledLDM() (bool, error) {
- result, err := dirContainsRegex("/usr/share/lightdm/lightdm.conf.d/", "allow-guest( |)=( |)false")
- if !result && err == nil {
- result, err = dirContainsRegex("/etc/lightdm/", "allow-guest( |)=( |)false")
- }
- return result, err
-}
-
-func programVersion(programName, versionNum string) (bool, error) {
- commandGiven := `dpkg -l | awk '$2=="` + programName + `" { print $3 }'`
- out, err := rawCmd(commandGiven).Output()
- if err != nil {
- return false, err
- }
- return strings.TrimSpace(string(out)) == versionNum, nil
-}
-
-func kernelVersion(version string) (bool, error) {
- return commandContains("uname -r", version)
-}
-
-func autoCheckUpdatesEnabled() (bool, error) {
- return dirContainsRegex("/etc/apt/apt.conf.d/", `APT::Periodic::Update-Package-Lists\s+"1";`)
-}
-
-// func permissionIs(filePath, permissionToCheck string) (bool, error) {
-// perm, err := commandOutput(`stat -c '%a' ` + filePath)
-// return perm == permissionToCheck, err
-// }
-
-func permissionIs(filePath, checkType, permissionToCheck string) (bool, error) {
- if checkType == "octal" {
- perm, err := commandOutput(`stat -c '%a' ` + filePath)
- return perm == permissionToCheck, err
- } else if checkType == "WorldWritable" {
- return commandContains(`find `+filePath+` -perm -g+w -or -perm -o+w`, filePath)
- } else if checkType == "WorldReadable" {
- return commandContains(`find `+filePath+` -perm -o=r`, filePath)
- }
- // If arguments are messed up or whatever:
- return false, nil
-}
diff --git a/cmd/checks_test.go b/cmd/checks_test.go
deleted file mode 100644
index 183a9c80..00000000
--- a/cmd/checks_test.go
+++ /dev/null
@@ -1,48 +0,0 @@
-package cmd
-
-import (
- "fmt"
- "testing"
-)
-
-func TestCommandOutput(t *testing.T) {
- out, err := commandOutput(`echo 1`)
- if err != nil || out != "1" {
- t.Error("commandOutput(`echo 1`, \"1\") got " + fmt.Sprint(out) + ", want `true`. Error " + err.Error())
- }
-}
-
-func TestCommandContains(t *testing.T) {
- out, err := commandContains(`echo hello world`, "hello")
- if err != nil || out != true {
- t.Error("commandContains(`echo hello world!`, \"hello\") got " + fmt.Sprint(out) + ", want `true`. Error " + err.Error())
- }
-}
-
-func TestPathExists(t *testing.T) {
- out, err := pathExists("/")
- if err != nil || out != true {
- t.Error("pathExists(\"/\") got " + fmt.Sprint(out) + ", want `true`. Error " + err.Error())
- }
-}
-
-func TestFileContains(t *testing.T) {
- out, err := fileContains("../misc/tests/TestFileContains.txt", "world")
- if err != nil || out != true {
- t.Error("fileContains(\"../misc/tests/TestFileContains.txt\", \"hello\") got " + fmt.Sprint(out) + ", want `true`. Error " + err.Error())
- }
-}
-
-func TestFileContainsRegex(t *testing.T) {
- out, err := fileContainsRegex("../misc/tests/TestFileContains.txt", "^hello")
- if err != nil || out != true {
- t.Error("fileContainsRegex(\"../misc/tests/TestFileContains.txt\", \"^hello\") got " + fmt.Sprint(out) + ", want `true`. Error: " + err.Error())
- }
-}
-
-func TestDirContainsRegex(t *testing.T) {
- out, err := dirContainsRegex("../misc/tests/dir", "^efgh")
- if err != nil || out != true {
- t.Error("dirContainsRegex(\"../misc/tests/dir\", \"^efgh\") got " + fmt.Sprint(out) + ", want `true`. Error " + err.Error())
- }
-}
diff --git a/cmd/checks_windows.go b/cmd/checks_windows.go
deleted file mode 100644
index 0e258270..00000000
--- a/cmd/checks_windows.go
+++ /dev/null
@@ -1,554 +0,0 @@
-package cmd
-
-import (
- "encoding/binary"
- "errors"
- "fmt"
- "os/exec"
- "regexp"
- "strconv"
- "strings"
-
- wapi "github.com/iamacarpet/go-win64api"
- "golang.org/x/sys/windows/registry"
-)
-
-// processCheck (Windows) will process Windows-specific checks handed to it
-// by the processCheckWrapper function.
-func processCheck(check *check, checkType, arg1, arg2, arg3 string) bool {
- switch checkType {
- case "UserDetail":
- if check.Message == "" {
- check.Message = "User property " + arg2 + " for " + arg1 + " is equal to \"" + arg3 + "\""
- }
- result, err := userDetail(arg1, arg2, arg3)
- return err == nil && result
- case "UserDetailNot":
- if check.Message == "" {
- check.Message = "User property " + arg2 + " for " + arg1 + " is not equal to \"" + arg3 + "\""
- }
- result, err := userDetail(arg1, arg2, arg3)
- return err == nil && !result
- case "UserRights":
- if check.Message == "" {
- check.Message = "User " + arg1 + " has privilege \"" + arg2 + "\""
- }
- result, err := userRights(arg1, arg2)
- return err == nil && result
- case "UserRightsNot":
- if check.Message == "" {
- check.Message = "User " + arg1 + " does not have privilege \"" + arg2 + "\""
- }
- result, err := userRights(arg1, arg2)
- return err == nil && !result
- case "ShareExists":
- if check.Message == "" {
- check.Message = "Share " + arg1 + " exists"
- }
- result, err := shareExists(arg1)
- return err == nil && result
- case "ShareExistsNot":
- if check.Message == "" {
- check.Message = "Share " + arg1 + " doesn't exist"
- }
- result, err := shareExists(arg1)
- return err == nil && !result
- case "ScheduledTaskExists":
- if check.Message == "" {
- check.Message = "Scheduled task " + arg1 + " exists"
- }
- result, err := scheduledTaskExists(arg1)
- return err == nil && result
- case "ScheduledTaskExistsNot":
- if check.Message == "" {
- check.Message = "Scheduled task " + arg1 + " doesn't exist"
- }
- result, err := scheduledTaskExists(arg1)
- return err == nil && !result
- /*
- case "StartupProgramExists":
- if check.Message == "" {
- check.Message = "Startup program " + arg1 + " exists"
- }
- result, err := startupProgramExists(arg1)
- return err == nil && result
- case "StartupProgramExistsNot":
- if check.Message == "" {
- check.Message = "Startup program " + arg1 + " doesn't exist"
- }
- result, err := scheduledTaskExists(arg1)
- return err == nil && !result
- */
- case "SecurityPolicy":
- if check.Message == "" {
- if arg3 != "" {
- check.Message = "Security policy option " + arg1 + " is between \"" + arg2 + "\" and \"" + arg3 + "\""
- }
- check.Message = "Security policy option " + arg1 + " is \"" + arg2 + "\""
- }
- result, err := securityPolicy(arg1, arg2, arg3)
- return err == nil && result
- case "SecurityPolicyNot":
- if check.Message == "" {
- if arg3 != "" {
- check.Message = "Security policy option " + arg1 + " is not between \"" + arg2 + "\" and \"" + arg3 + "\""
- }
- check.Message = "Security policy option " + arg1 + " is not \"" + arg2 + "\""
- }
- result, err := securityPolicy(arg1, arg2, arg3)
- return err == nil && !result
- case "RegistryKey":
- if check.Message == "" {
- check.Message = "Registry key " + arg1 + " matches \"" + arg2 + "\""
- }
- result, err := registryKey(arg1, arg2, false)
- return err == nil && result
- case "RegistryKeyNot":
- if check.Message == "" {
- check.Message = "Registry key " + arg1 + " does not match \"" + arg2 + "\""
- }
- result, err := registryKey(arg1, arg2, false)
- return err == nil && !result
- case "RegistryKeyExists":
- if check.Message == "" {
- check.Message = "Registry key " + arg1 + " exists"
- }
- result, err := registryKey(arg1, arg2, true)
- return err == nil && result
- case "RegistryKeyExistsNot":
- if check.Message == "" {
- check.Message = "Registry key " + arg1 + " does not exist"
- }
- result, err := registryKey(arg1, arg2, true)
- return err == nil && !result
- case "PasswordChanged":
- if check.Message == "" {
- check.Message = "Password for " + arg1 + " has been changed"
- }
- result, err := passwordChanged(arg1, arg2)
- return err == nil && result
- case "PasswordChangedNot":
- if check.Message == "" {
- check.Message = "Password for " + arg1 + " has not been changed"
- }
- result, err := passwordChanged(arg1, arg2)
- return err == nil && !result
- case "WindowsFeature":
- if check.Message == "" {
- check.Message = arg1 + " feature has been enabled"
- }
- result, err := windowsFeature(arg1)
- return err == nil && result
- case "WindowsFeatureNot":
- if check.Message == "" {
- check.Message = arg1 + " feature has been disabled"
- }
- result, err := windowsFeature(arg1)
- return err == nil && !result
- case "FileOwner":
- if check.Message == "" {
- check.Message = arg1 + " is owned by " + arg2
- }
- result, err := fileOwner(arg1, arg2)
- return err == nil && result
- case "FileOwnerNot":
- if check.Message == "" {
- check.Message = arg1 + " is not owned by " + arg2
- }
- result, err := fileOwner(arg1, arg2)
- return err == nil && !result
- case "ServiceStatus":
- if check.Message == "" {
- check.Message = "The service " + arg1 + " is " + arg2 + " with the startup type set as " + arg3
- }
- result, err := serviceStatus(arg1, arg2, arg3)
- return err == nil && result
- case "ServiceStatusNot":
- if check.Message == "" {
- check.Message = "The service " + arg1 + " is not " + arg2 + " with the startup type not set as " + arg3
- }
- result, err := serviceStatus(arg1, arg2, arg3)
- return err == nil && !result
- case "BitlockerEnabled":
- if check.Message == "" {
- check.Message = "Bitlocker drive encryption has been enabled"
- }
- result, err := bitlockerEnabled()
- return err == nil && result
- case "BitlockerEnabledNot":
- if check.Message == "" {
- check.Message = "Bitlocker drive encryption has been disabled"
- }
- result, err := bitlockerEnabled()
- return err == nil && !result
- default:
- failPrint("No check type " + checkType)
- }
- return false
-}
-
-func command(commandGiven string) (bool, error) {
- cmd := rawCmd(commandGiven + "; if (!($?)) { Throw 'Error' }")
- if err := cmd.Run(); err != nil {
- if _, ok := err.(*exec.ExitError); ok {
- return false, nil
- }
- }
- return true, nil
-}
-
-func programInstalled(programName string) (bool, error) {
- programList, err := getPrograms()
- if err != nil {
- return false, err
- }
- for _, p := range programList {
- if strings.Contains(p, programName) {
- return true, nil
- }
- }
- return false, nil
-}
-
-func programVersion(programName, versionNum string) (bool, error) {
- prog, err := getProgram(programName)
- if err != nil {
- return false, err
- }
- return prog.DisplayVersion == versionNum, nil
-}
-
-func serviceUp(serviceName string) (bool, error) {
- serviceStatus, err := getLocalServiceStatus(serviceName)
- return serviceStatus.IsRunning, err
-}
-
-func serviceStatus(serviceName, wantedStatus, startupType string) (bool, error) {
- status, err := getLocalServiceStatus(serviceName)
- var boolWantedStatus bool
- if err != nil {
- return false, err
- }
- switch wantedStatus = strings.ToLower(wantedStatus); wantedStatus {
- case "running":
- boolWantedStatus = true
- case "stopped":
- boolWantedStatus = false
- default:
- errMessage := "Unknown status type found for " + serviceName
- failPrint(errMessage)
- return false, errors.New(errMessage)
- }
- if status.IsRunning == boolWantedStatus {
- serviceKey := `HKLM\SYSTEM\CurrentControlSet\Services\` + serviceName + `\Start`
- var wantedStartupTypeNumber string
- switch startupType = strings.ToLower(startupType); startupType {
- case "automatic":
- wantedStartupTypeNumber = "2"
- case "manual":
- wantedStartupTypeNumber = "3"
- case "disabled":
- wantedStartupTypeNumber = "4"
- default:
- failPrint("Unknown startup type found for " + serviceName)
- return false, errors.New("Unknown status type found for " + serviceName)
- }
- check, err := registryKey(serviceKey, wantedStartupTypeNumber, false)
- if err != nil {
- return false, err
- }
- if check {
- return true, nil
- }
- }
- return false, err
-}
-
-func passwordChanged(user, date string) (bool, error) {
- changed, _ := commandOutput(`(Get-LocalUser ` + user + ` | select PasswordLastSet).PasswordLastSet -replace "n",", " -replace "r",", "`)
- return changed >= date, nil
-}
-
-func windowsFeature(feature string) (bool, error) {
- state, _ := commandOutput("(Get-WindowsOptionalFeature -FeatureName " + feature + " -Online).State")
- return state == "Enabled", nil
-}
-
-func fileOwner(filePath, owner string) (bool, error) {
- theowner, _ := commandOutput("(Get-Acl " + filePath + ").Owner")
- return theowner == owner, nil
-}
-
-func userExists(userName string) (bool, error) {
- user, err := getLocalUser(userName)
- if err != nil {
- return false, err
- }
- if user.Username == "" {
- return false, nil
- }
- return true, nil
-}
-
-func userInGroup(userName, groupName string) (bool, error) {
- users, err := wapi.LocalGroupGetMembers(groupName)
- if err != nil {
- // Error is returned if group is empty.
- return false, nil
- }
- for _, user := range users {
- justName := strings.Split(user.Name, `\`)[1]
- if userName == user.Name || userName == justName {
- return true, nil
- }
- }
- return false, nil
-}
-
-func firewallUp() (bool, error) {
- fwProfilesInt := []int{wapi.NET_FW_PROFILE2_DOMAIN, wapi.NET_FW_PROFILE2_PRIVATE, wapi.NET_FW_PROFILE2_PUBLIC}
- for profile := range fwProfilesInt {
- profileResult, err := wapi.FirewallIsEnabled(int32(profile))
- if err != nil {
- return false, err
- } else if !profileResult {
- return false, nil
- }
- }
- return true, nil
-}
-
-func bitlockerEnabled() (bool, error) {
- const FULLY_ENCRYPTED = 1
- const ENCRYPTION_IN_PROGRESS = 2
- status, err := wapi.GetBitLockerConversionStatusForDrive("C:")
- if err == nil {
- if status.ConversionStatus == FULLY_ENCRYPTED || status.ConversionStatus == ENCRYPTION_IN_PROGRESS {
- return true, nil
- }
- }
- return false, nil
-}
-
-func userDetail(userName, detailName, detailValue string) (bool, error) {
- detailValue = strings.TrimSpace(detailValue)
- lookingFor := false
- if strings.ToLower(detailValue) == "yes" {
- lookingFor = true
- }
- user, err := getLocalUser(userName)
- if err != nil {
- return false, err
- }
- switch detailName {
- case "FullName":
- if user.FullName == detailValue {
- return true, nil
- }
- case "IsEnabled":
- return user.IsEnabled == lookingFor, nil
- case "IsLocked":
- return user.IsLocked == lookingFor, nil
- case "IsAdmin":
- return user.IsAdmin == lookingFor, nil
- case "PasswordNeverExpires":
- return user.PasswordNeverExpires == lookingFor, nil
- default:
- failPrint("detailName (" + detailName + ") passed to userDetail is invalid.")
- return false, errors.New("Invalid detailName")
- }
- return false, nil
-}
-
-func userRights(userOrGroup, privilege string) (bool, error) {
- // todo consider /mergedpolicy when windows domain is active?
- // domain support is untested, it should be easy to add a domain
- // flag in the config though. then just make sure you're not getting
- // invalid local policies instead of gpo
-
- seceditOutput, err := getSecedit()
- // TODO: only get section of users -- this can also falsely score correct for other secedit fields (like LegalNoticeText)
- if err != nil {
- return false, err
- }
- re := regexp.MustCompile("(?m)[\r\n]+^.*" + privilege + ".*$")
- privilegeString := string(re.Find([]byte(seceditOutput)))
- if privilegeString == "" {
- return false, nil
- }
- if strings.Contains(privilegeString, userOrGroup) {
- // Sometimes, Windows just puts their user or group name instead of the SID. Real cool
- return true, nil
- }
- privStringSplit := strings.Split(privilegeString, " ")
- if len(privStringSplit) != 3 {
- return false, errors.New("Error splitting privilege")
- }
- privStringSplit = strings.Split(privStringSplit[2], ",")
- for _, sidValue := range privStringSplit {
- sidValue = strings.TrimSpace(sidValue)
- userForSid := strings.Split(sidToLocalUser(sidValue[1:]), "\\")
- userSid := strings.TrimSpace(userForSid[0])
- if len(userForSid) == 2 {
- userSid = strings.TrimSpace(userForSid[1])
- }
- if userSid == userOrGroup {
- return true, nil
- }
- }
- return false, err
-}
-
-func shareExists(shareName string) (bool, error) {
- return command("Get-SmbShare -Name '" + shareName + "'")
-}
-
-func scheduledTaskExists(taskName string) (bool, error) {
- return command("Get-ScheduledTask -TaskName '" + taskName + "'")
-}
-
-func startupProgramExists(progName string) (bool, error) {
- // need to work out the implementation on this one too...
- // multiple startup locations
- // rot
- return true, nil
-}
-
-func securityPolicy(keyName, keyValue, optValue string) (bool, error) {
- var desiredString string
- if regKey, ok := secpolToKey[keyName]; ok {
- return registryKey(regKey, keyValue, false)
- }
- seceditOutput, err := getSecedit()
- if err != nil {
- return false, err
- }
- re := regexp.MustCompile("(?m)[\r\n]+^.*" + keyName + ".*$")
- output := strings.TrimSpace(string(re.Find([]byte(seceditOutput))))
- if output == "" {
- return false, errors.New("SecurityPolicy item not found")
- }
- if keyName == "NewAdministratorName" || keyName == "NewGuestName" {
- // These two are strings, not numbers, so they have ""
- desiredString = keyName + " = " + keyValue
- } else if keyName == "MinimumPasswordAge" ||
- keyName == "MinimumPasswordLength" ||
- keyName == "LockoutDuration" ||
- keyName == "ResetLockoutCount" ||
- keyName == "MaximumPasswordAge" ||
- keyName == "LockoutBadCount" {
- // Fields where the arg should be X or higher (up to 999)
- intLow, err := strconv.Atoi(keyValue)
- if err != nil {
- failPrint(keyValue + " is not a valid integer for SecurityPolicy check")
- return false, errors.New("Invalid keyValue")
- }
- var result1, e = strconv.Atoi(strings.Split(output, " = ")[1])
- if e != nil {
- return false, e
- }
- if optValue != "" {
- intHigh, err := strconv.Atoi(optValue)
- if err != nil {
- failPrint(optValue + " is not a valid integer for SecurityPolicy check")
- return false, errors.New("Invalid optValue")
- }
- if intLow <= result1 && result1 <= intHigh {
- return true, nil
- }
- } else {
- if result1 == intLow {
- return true, nil
- }
- }
-
- } else {
- desiredString = keyName + " = " + keyValue
- }
- return output == desiredString, nil
-}
-
-func registryKey(keyName, keyValue string, existCheck bool) (bool, error) {
- // Break down input
- registryArgs := regexp.MustCompile(`[\\]+`).Split(keyName, -1)
- registryHiveText := registryArgs[0]
- keyPath := fmt.Sprintf(strings.Join(registryArgs[1:len(registryArgs)-1], "\\")) // idk??
- keyLoc := registryArgs[len(registryArgs)-1]
- // fmt.Printf("REGISTRY: getting keypath %s from %s\n", keyPath, registryHiveText)
-
- var registryHive registry.Key
- switch registryHiveText {
- case "HKEY_CLASSES_ROOT", "HKCR":
- registryHive = registry.CLASSES_ROOT
- case "HKEY_CURRENT_USER", "HKCU":
- registryHive = registry.CURRENT_USER
- case "HKEY_LOCAL_MACHINE", "HKLM", "MACHINE":
- registryHive = registry.LOCAL_MACHINE
- case "HKEY_USERS", "HKU":
- registryHive = registry.USERS
- case "HKEY_CURRENT_CONFIG", "HKCC":
- registryHive = registry.CURRENT_CONFIG
- case "SOFTWARE":
- registryHive = registry.LOCAL_MACHINE
- keyPath = "SOFTWARE\\" + keyPath
- default:
- if existCheck {
- return false, nil
- }
- failPrint("Unknown registry hive: " + registryHiveText)
- return false, errors.New("Unknown registry hive" + registryHiveText)
- }
-
- // Actually get the key
- k, err := registry.OpenKey(registryHive, keyPath, registry.QUERY_VALUE)
- if err != nil {
- if existCheck {
- return false, nil
- }
- warnPrint("Registry opening key failed (and that's probably fine): " + err.Error())
- return false, err
- }
- defer k.Close()
-
- // Fetch registry value
- registrySlice := make([]byte, 256)
- regLength, valType, err := k.GetValue(keyLoc, registrySlice)
- if err != nil {
- // Error is probably about the key not existing.
- // This is fine, some keys are not defined until the setting
- // is explicitly set. However, the check should not pass
- // for RegistryKey or RegistryKeyNot, so we return an error.
- if existCheck {
- return false, nil
- }
- warnPrint("Registry opening key failed (and that's probably fine): " + err.Error())
- return false, err
- }
- if existCheck {
- return true, nil
- }
-
- registrySlice = registrySlice[:regLength]
- // fmt.Printf("Retrieved registry value was %d (length %d, type %d)\n", registrySlice, regLength, valType)
-
- // Determine value type to convert to string
- var registryValue string
- switch valType {
- case 1: // SZ
- registryValue, _, err = k.GetStringValue(keyLoc)
- case 2: // EXPAND_SZ
- registryValue, _, err = k.GetStringValue(keyLoc)
- case 3: // BINARY
- failPrint("Binary registry format not yet supported.")
- case 4: // DWORD
- registryValue = strconv.FormatUint(uint64(binary.LittleEndian.Uint32(registrySlice)), 10)
- default:
- failPrint("Unknown registry type: " + fmt.Sprint(valType))
- }
-
- // fmt.Printf("Registry value: %s, keyvalue %s\n", registryValue, keyValue)
- if registryValue == keyValue {
- return true, err
- }
- return false, err
-}
diff --git a/cmd/configs.go b/cmd/configs.go
deleted file mode 100644
index 6964885c..00000000
--- a/cmd/configs.go
+++ /dev/null
@@ -1,277 +0,0 @@
-package cmd
-
-import (
- "bytes"
- "encoding/hex"
- "errors"
- "fmt"
- "os"
- "strings"
-
- "github.com/BurntSushi/toml"
- "github.com/fatih/color"
-)
-
-// parseConfig takes the config content as a string and attempts to parse it
-// into the mc.Config struct based on the TOML spec.
-func parseConfig(configContent string) {
- if configContent == "" {
- failPrint("Configuration is empty!")
- os.Exit(1)
- }
-
- if _, err := toml.Decode(configContent, &mc.Config); err != nil {
- failPrint("error decoding TOML: " + err.Error())
- os.Exit(1)
- }
-
- // If there's no remote, local must be enabled.
- if mc.Config.Remote == "" {
- mc.Config.Local = true
- }
-
- if mc.Config.Remote != "" {
- if mc.Config.Name == "" {
- failPrint("Need image name in config if remote is enabled.")
- os.Exit(1)
- }
- }
-
- // This is probably going to be constantly throwing warnings because we never actually update the version :upside_down:
- if mc.Config.Version != AeacusVersion {
- warnPrint("Scoring version does not match Aeacus version! Compatability issues may occur.")
- }
-}
-
-// WriteConfig reads the plaintext configuration from sourceFile, and writes
-// the encrypted configuration into the destFile name passed.
-func WriteConfig(sourceFile, destFile string) {
- infoPrint("Reading configuration from " + mc.DirPath + sourceFile + "...")
-
- configFile, err := readFile(mc.DirPath + sourceFile)
- if err != nil {
- failPrint("Can't open scoring configuration file (" + sourceFile + "): " + err.Error())
- os.Exit(1)
- }
- parseConfig(configFile)
- configFile = ""
-
- obfuscateConfig()
- buf := new(bytes.Buffer)
- if err := toml.NewEncoder(buf).Encode(mc.Config); err != nil {
- failPrint(err.Error())
- os.Exit(1)
- return
- }
-
- encryptedConfig, err := encryptConfig(buf.String())
- if err != nil {
- failPrint("Encrypting config failed: " + err.Error())
- os.Exit(1)
- } else if verboseEnabled {
- infoPrint("Writing data to " + mc.DirPath + "...")
- }
-
- writeFile(mc.DirPath+destFile, encryptedConfig)
-}
-
-// readData is a wrapper around decryptData, taking the scoring data fileName,
-// and reading its content. It returns the decrypt config.
-func readData() (string, error) {
- // Read in the encrypted configuration filei
- dataFile, err := readFile(mc.DirPath + ScoringData)
- if err != nil {
- return "", err
- } else if dataFile == "" {
- return "", errors.New("Scoring data is empty!")
- }
- decryptedConfig, err := decryptConfig(dataFile)
- if err != nil {
- return "", err
- }
- return decryptedConfig, nil
-}
-
-// printConfig offers a printed representation of the config, as parsed
-// by readData and parseConfig.
-func printConfig() {
- passPrint("Configuration " + mc.DirPath + ScoringConf + " check passed!")
- fmt.Println("Aeacus Version:", mc.Config.Version)
- fmt.Println("Title:", mc.Config.Title)
- fmt.Println("Name:", mc.Config.Name)
- fmt.Println("OS:", mc.Config.OS)
- fmt.Println("User:", mc.Config.User)
- fmt.Println("Remote:", mc.Config.Remote)
- fmt.Println("Local:", mc.Config.Local)
- fmt.Println("EndDate:", mc.Config.EndDate)
- fmt.Println("NoDestroy:", mc.Config.NoDestroy)
- fmt.Println("DisableShell:", mc.Config.DisableShell)
- fmt.Println("Checks:")
- for i, check := range mc.Config.Check {
- fmt.Printf("\tCheck %d (%d points):\n", i+1, check.Points)
- fmt.Println("\t\tMessage:", check.Message)
- if check.Pass != nil {
- fmt.Println("\t\tPassConditions:")
- for _, condition := range check.Pass {
- fmt.Printf("\t\t\t%s: %s %s %s %s\n", condition.Type, condition.Arg1, condition.Arg2, condition.Arg3, condition.Arg4)
- }
- }
- if check.PassOverride != nil {
- fmt.Println("\t\tPassOverrideConditions:")
- for _, condition := range check.PassOverride {
- fmt.Printf("\t\t\t%s: %s %s %s %s\n", condition.Type, condition.Arg1, condition.Arg2, condition.Arg3, condition.Arg4)
- }
- }
- if check.Fail != nil {
- fmt.Println("\t\tFailConditions:")
- for _, condition := range check.Fail {
- fmt.Printf("\t\t\t%s: %s %s %s %s\n", condition.Type, condition.Arg1, condition.Arg2, condition.Arg3, condition.Arg4)
- }
- }
- }
-}
-
-func obfuscateConfig() {
- infoPrint("Obfuscating configuration...")
- if err := obfuscateData(&mc.Config.Password); err != nil {
- errorPrint(err)
- }
- for i, check := range mc.Config.Check {
- if err := obfuscateData(&mc.Config.Check[i].Message); err != nil {
- errorPrint(err)
- }
- if check.Pass != nil {
- for x := range check.Pass {
- if err := obfuscateData(&mc.Config.Check[i].Pass[x].Type); err != nil {
- errorPrint(err)
- }
- if err := obfuscateData(&mc.Config.Check[i].Pass[x].Arg1); err != nil {
- errorPrint(err)
- }
- if err := obfuscateData(&mc.Config.Check[i].Pass[x].Arg2); err != nil {
- errorPrint(err)
- }
- if err := obfuscateData(&mc.Config.Check[i].Pass[x].Arg3); err != nil {
- errorPrint(err)
- }
- if err := obfuscateData(&mc.Config.Check[i].Pass[x].Arg4); err != nil {
- errorPrint(err)
- }
- }
- }
- if check.PassOverride != nil {
- for x := range check.PassOverride {
- if err := obfuscateData(&mc.Config.Check[i].PassOverride[x].Type); err != nil {
- errorPrint(err)
- }
- if err := obfuscateData(&mc.Config.Check[i].PassOverride[x].Arg1); err != nil {
- errorPrint(err)
- }
- if err := obfuscateData(&mc.Config.Check[i].PassOverride[x].Arg2); err != nil {
- errorPrint(err)
- }
- if err := obfuscateData(&mc.Config.Check[i].PassOverride[x].Arg3); err != nil {
- errorPrint(err)
- }
- if err := obfuscateData(&mc.Config.Check[i].PassOverride[x].Arg4); err != nil {
- errorPrint(err)
- }
- }
- }
- if check.Fail != nil {
- for x := range check.Fail {
- if err := obfuscateData(&mc.Config.Check[i].Fail[x].Type); err != nil {
- errorPrint(err)
- }
- if err := obfuscateData(&mc.Config.Check[i].Fail[x].Arg1); err != nil {
- errorPrint(err)
- }
- if err := obfuscateData(&mc.Config.Check[i].Fail[x].Arg2); err != nil {
- errorPrint(err)
- }
- if err := obfuscateData(&mc.Config.Check[i].Fail[x].Arg3); err != nil {
- errorPrint(err)
- }
- if err := obfuscateData(&mc.Config.Check[i].Fail[x].Arg4); err != nil {
- errorPrint(err)
- }
- }
- }
- }
-}
-
-// ConfirmPrint will prompt the user with the given toPrint string, and
-// exit the program if N or n is input.
-func ConfirmPrint(toPrint string) {
- printer(color.FgYellow, "CONF", "")
- fmt.Print(toPrint + " [Y/n]: ")
- var resp string
- fmt.Scanln(&resp)
- if strings.ToLower(strings.TrimSpace(resp)) == "n" {
- os.Exit(1)
- }
-}
-
-func passPrint(toPrint string) {
- if verboseEnabled {
- printStr := printer(color.FgGreen, "PASS", toPrint)
- fmt.Printf(printStr)
- }
-}
-
-func failPrint(toPrint string) {
- fmt.Printf(printer(color.FgRed, "FAIL", toPrint))
-}
-
-func warnPrint(toPrint string) {
- fmt.Printf(printer(color.FgYellow, "WARN", toPrint))
-}
-
-func debugPrint(toPrint string) {
- if debugEnabled {
- printStr := printer(color.FgMagenta, "DEBUG", toPrint)
- fmt.Printf(printStr)
- }
-}
-
-func infoPrint(toPrint string) {
- if verboseEnabled {
- printStr := printer(color.FgCyan, "INFO", toPrint)
- fmt.Printf(printStr)
- }
-}
-
-func errorPrint(err error) {
- fmt.Printf(printer(color.FgRed, "ERR", err.Error()))
-}
-
-func printer(colorChosen color.Attribute, messageType, toPrint string) string {
- printer := color.New(colorChosen, color.Bold)
- printStr := "["
- printStr += printer.Sprintf(messageType)
- printStr += fmt.Sprintf("] %s", toPrint)
- if toPrint != "" {
- printStr += "\n"
- }
- return printStr
-}
-
-func xor(key, plaintext string) string {
- ciphertext := make([]byte, len(plaintext))
- for i := 0; i < len(plaintext); i++ {
- ciphertext[i] = key[i%len(key)] ^ plaintext[i]
- }
- return string(ciphertext)
-}
-
-func hexEncode(inputString string) string {
- return hex.EncodeToString([]byte(inputString))
-}
-
-func hexDecode(inputString string) (string, error) {
- result, err := hex.DecodeString(inputString)
- if err != nil {
- return "", err
- }
- return string(result), nil
-}
diff --git a/cmd/crypto.go b/cmd/crypto.go
deleted file mode 100644
index 1449fa3a..00000000
--- a/cmd/crypto.go
+++ /dev/null
@@ -1,132 +0,0 @@
-// crypto.go is an example to provides basic cryptographical functions
-// for aeacus.
-//
-// This file is not a shining example for cryptographically secure operations.
-//
-// Practically, it is more important that your implemented solution is
-// different than the example, to make reverse engineering much more difficult.
-//
-// You could radically change the crypto.go file each time you release an
-// image, which would make things very difficult for a would-be hacker.
-//
-// At the very least, edit some strings. Add some ciphers and operations if
-// you're feeling spicy.
-
-package cmd
-
-import (
- "bytes"
- "compress/zlib"
- "errors"
- "io"
-)
-
-// These hashes are used for XORing the plaintext. Again-- not
-// cryptographically genius.
-const (
- randomHashOne = "these hashes will be changed automatically"
- randomHashTwo = "if you run misc/dev/gen-crypto.sh / run the build script"
-)
-
-var byteKey = []byte{0x01, 0x02, 0x03}
-
-// encryptConfig takes the plainText config and returns an encrypted string
-// that should be written to the encrypted scoring data file.
-func encryptConfig(plainText string) (string, error) {
- // Generate key by XORing two strings.
- key := xor(randomHashOne, randomHashTwo)
-
- // Compress the file with zlib.
- var encryptedFile bytes.Buffer
- writer := zlib.NewWriter(&encryptedFile)
-
- // Write zlib compressed data into encryptedFile
- _, err := writer.Write([]byte(plainText))
- if err != nil {
- return "", err
- }
- writer.Close()
-
- // XOR the encrypted file with our key.
- return xor(key, encryptedFile.String()), err
-}
-
-// decryptConfig is used to decrypt the scoring data file.
-func decryptConfig(cipherText string) (string, error) {
- // Create our key by XORing two strings.
- key := xor(randomHashOne, randomHashTwo)
-
- // Apply the XOR key to decrypt the zlib-compressed data.
- cipherText = xor(key, cipherText)
-
- // Create the zlib reader.
- reader, err := zlib.NewReader(bytes.NewReader([]byte(cipherText)))
- if err != nil {
- return "", errors.New("error creating zlib reader")
- }
- defer reader.Close()
-
- // Read into our created buffer.
- dataBuffer := bytes.NewBuffer(nil)
- _, err = io.Copy(dataBuffer, reader)
- if err != nil {
- failPrint("error decompressing scoring data")
- return "", errors.New("error decompressing zlib data")
- }
-
- // Check that decryptedConfig is not empty.
- decryptedConfig := dataBuffer.String()
- if decryptedConfig == "" {
- return "", errors.New("decrypted config is empty")
- }
-
- return decryptedConfig, err
-}
-
-// tossKey is responsible for changing up the byteKey.
-func tossKey() []byte {
- // Add your cool byte array manipulations here!
- return byteKey
-}
-
-// obfuscateData encodes the configuration when writing to ScoringData.
-// This also makes manipulation of data in use harder, since there is
-// a very small opportunity for catching plaintext data, and very tough
-// to decode the decrypted ScoringData without source code.
-func obfuscateData(datum *string) error {
- var err error
- if *datum == "" {
- debugPrint("empty datum given to obfuscateData")
- return nil
- }
- if *datum, err = encryptConfig(*datum); err == nil {
- *datum = hexEncode(xor(string(tossKey()), *datum))
- } else {
- failPrint("crypto: failed to obufscate datum: " + err.Error())
- return err
- }
- return nil
-}
-
-// deobfuscateData decodes configuration data.
-func deobfuscateData(datum *string) error {
- var err error
- if *datum == "" {
- // empty data given to deobfuscateData-- not really a concern
- // often this is just empty/optional struct fields
- debugPrint("empty datum given to deobfuscateData")
- return nil
- }
- *datum, err = hexDecode(*datum)
- if err != nil {
- println(*datum)
- failPrint("crypto: failed to deobfuscate datum hex: " + err.Error())
- return err
- }
- *datum = xor(string(tossKey()), *datum)
- if *datum, err = decryptConfig(*datum); err != nil {
- failPrint("crypto: failed to deobufscate datum: " + err.Error())
- return err
- }
- return nil
-}
diff --git a/cmd/gui_linux.go b/cmd/gui_linux.go
deleted file mode 100644
index a62ba148..00000000
--- a/cmd/gui_linux.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package cmd
-
-func LaunchIDPrompt() {
- warnPrint("This is not implemented on linux")
-}
-
-func LaunchConfigGui() {
- warnPrint("This is not implemented on linux")
-}
diff --git a/cmd/info_linux.go b/cmd/info_linux.go
deleted file mode 100644
index 3e127587..00000000
--- a/cmd/info_linux.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package cmd
-
-func GetInfo(infoType string) {
- warnPrint("Info gathering is not supported for Linux-- there's always a better, easier command-line tool.")
-}
diff --git a/cmd/release_linux.go b/cmd/release_linux.go
deleted file mode 100644
index 32edb1f9..00000000
--- a/cmd/release_linux.go
+++ /dev/null
@@ -1,109 +0,0 @@
-package cmd
-
-// WriteDesktopFiles creates TeamID.txt and its shortcut, as well as links
-// to the ScoringReport, ReadMe, and other needed files.
-func WriteDesktopFiles() {
- infoPrint("Creating or emptying TeamID.txt...")
- shellCommand("echo 'YOUR-TEAMID-HERE' > " + mc.DirPath + "TeamID.txt")
- shellCommand("chmod 666 " + mc.DirPath + "TeamID.txt")
- shellCommand("chown " + mc.Config.User + ":" + mc.Config.User + " " + mc.DirPath + "TeamID.txt")
- infoPrint("Writing shortcuts to Desktop...")
- shellCommand("cp " + mc.DirPath + "misc/desktop/*.desktop /home/" + mc.Config.User + "/Desktop/")
- shellCommand("chmod +x /home/" + mc.Config.User + "/Desktop/*.desktop")
- shellCommand("chown " + mc.Config.User + ":" + mc.Config.User + " /home/" + mc.Config.User + "/Desktop/*")
-}
-
-// ConfigureAutologin configures the auto-login capability for LightDM and
-// GDM3, so that the image automatically logs in to the main user's account
-// on boot.
-func ConfigureAutologin() {
- lightdm, _ := pathExists("/usr/share/lightdm")
- gdm, _ := pathExists("/etc/gdm3/")
- if lightdm {
- infoPrint("LightDM detected for autologin.")
- shellCommand(`echo "autologin-user=` + mc.Config.User + `" >> /usr/share/lightdm/lightdm.conf.d/50-ubuntu.conf`)
- } else if gdm {
- infoPrint("GDM3 detected for autologin.")
- shellCommand(`echo -e "AutomaticLoginEnable=True\nAutomaticLogin=` + mc.Config.User + `" >> /etc/gdm3/daemon.conf`)
- } else {
- failPrint("Unable to configure autologin! Please do so manually.")
- }
-}
-
-// InstallFont is skipped for Linux Builds
-func InstallFont() {
- infoPrint("Skipping font install for Linux...")
-}
-
-// InstallService for Linux installs and starts the CSSClient init.d service.
-func InstallService() {
- infoPrint("Installing service...")
- shellCommand("cp " + mc.DirPath + "misc/dev/CSSClient /etc/init.d/")
- shellCommand("chmod +x /etc/init.d/CSSClient")
- shellCommand("systemctl enable CSSClient")
- shellCommand("systemctl start CSSClient")
-}
-
-// CleanUp for Linux is primarily focused on removing cached files, history,
-// and other pieces of forensic evidence. It also removes the non-required
-// files in the aeacus directory.
-func CleanUp() {
- infoPrint("Removing .keys file...")
- removeKeys(LinuxDir)
-
- infoPrint("Installing BleachBit...")
- shellCommand("apt-get install -y bleachbit")
-
- findPaths := "/bin /etc /home /opt /root /sbin /srv /usr /mnt /var"
-
- infoPrint("Changing perms to 755 in " + mc.DirPath + "...")
- shellCommand("chmod 755 -R " + mc.DirPath)
-
- infoPrint("Removing .viminfo and .swp files...")
- shellCommand("find " + findPaths + " -iname '*.viminfo*' -delete -iname '*.swp' -delete")
-
- infoPrint("Symlinking .bash_history and .zsh_history to /dev/null...")
- shellCommand(`find ` + findPaths + ` -iname '*.bash_history' -exec ln -sf /dev/null {} \;`)
- shellCommand(`find ` + findPaths + ` -name '.zsh_history' -exec ln -sf /dev/null {} \;`)
-
- infoPrint("Removing .local files...")
- shellCommand("rm -rf /root/.local /home/*/.local/")
-
- infoPrint("Removing cache...")
- shellCommand("rm -rf /root/.cache /home/*/.cache/")
-
- infoPrint("Removing temp root and Desktop files...")
- shellCommand("rm -rf /root/*~ /home/*/Desktop/*~")
-
- infoPrint("Removing crash and VMWare data...")
- shellCommand("rm -f /var/VMwareDnD/* /var/crash/*.crash")
-
- infoPrint("Removing apt and dpkg logs...")
- shellCommand("rm -rf /var/log/apt/* /var/log/dpkg.log")
-
- infoPrint("Removing logs (auth and syslog)...")
- shellCommand("rm -f /var/log/auth.log* /var/log/syslog*")
-
- infoPrint("Removing initial package list...")
- shellCommand("rm -f /var/log/installer/initial-status.gz")
-
- infoPrint("Removing scoring.conf...")
- shellCommand("rm " + mc.DirPath + "scoring.conf*")
-
- infoPrint("Removing other setup files...")
- shellCommand("rm -rf " + mc.DirPath + "misc/")
- shellCommand("rm -rf " + mc.DirPath + "ReadMe.conf")
- shellCommand("rm -rf " + mc.DirPath + "README.md")
- shellCommand("rm -rf " + mc.DirPath + "TODO.md")
- shellCommand("rm -rf " + mc.DirPath + ".git")
- shellCommand("rm -rf " + mc.DirPath + ".github")
-
- infoPrint("Removing aeacus binary...")
- shellCommand("rm " + mc.DirPath + "aeacus")
-
- infoPrint("Overwriting timestamps to obfuscate changes...")
- shellCommand(`find /etc /home /var -exec touch --date='2012-12-12 12:12' {} \; 2>/dev/null`)
-
- infoPrint("Clearing firefox cache and browsing history...")
- shellCommand("bleachbit --clean firefox.url_history; bleachbit --clean firefox.cache")
-}
diff --git a/cmd/routines.go b/cmd/routines.go
deleted file mode 100644
index 6abf0244..00000000
--- a/cmd/routines.go
+++ /dev/null
@@ -1,87 +0,0 @@
-package cmd
-
-import (
- "errors"
- "os"
- "runtime"
-
- "github.com/urfave/cli/v2"
-)
-
-// ReadScoringData is a convenience function around readData and decodeString,
-// which parses the encrypted scoring configuration file.
-func ReadScoringData() error {
- infoPrint("Decrypting data from " + mc.DirPath + ScoringData + "...")
- decryptedData, err := readData()
- if err != nil {
- failPrint("error reading in scoring data: " + err.Error())
- return err
- } else if decryptedData == "" {
- failPrint("scoring data is empty! Is the file corrupted?")
- return errors.New("Scoring data is empty!")
- } else {
- infoPrint("Data decrypting successful!")
- }
- parseConfig(decryptedData)
- return nil
-}
-
-// CheckConfig parses and checks the validity of the current ScoringConf file.
-func CheckConfig(fileName string) {
- fileContent, err := readFile(mc.DirPath + fileName)
- if err != nil {
- failPrint("Configuration file (" + fileName + ") not found!")
- os.Exit(1)
- }
- parseConfig(fileContent)
- printConfig()
- obfuscateConfig()
-}
-
-// FillConstants determines the correct constants, such as DirPath, for the
-// given runtime and environment.
-func FillConstants() {
- if runtime.GOOS == "linux" {
- mc.DirPath = LinuxDir
- } else if runtime.GOOS == "windows" {
- mc.DirPath = WindowsDir
- } else {
- failPrint("This operating system (" + runtime.GOOS + ") is not supported!")
- os.Exit(1)
- }
-}
-
-// ScoreImage is the main function for scoring the image
-func ScoreImage() {
- checkTrace()
- timeCheck()
- infoPrint("Scoring image...")
- scoreImage()
-}
-
-// RunningPermsCheck is a convenience function wrapper around
-// adminCheck, which prints an error indicating that admin
-// permissions are needed.
-func RunningPermsCheck() {
- if !adminCheck() {
- failPrint("You need to run this binary as root or Administrator!")
- os.Exit(1)
- }
-}
-
-// ParseFlags sets the global variable values, for example, verboseEnabled.
-func ParseFlags(c *cli.Context) {
- if c.Bool("v") {
- verboseEnabled = true
- }
- if c.Bool("d") {
- debugEnabled = true
- }
- if c.Bool("y") {
- YesEnabled = true
- }
-}
-
-func SetVerbose(val bool) {
- verboseEnabled = val
-}
diff --git a/cmd/scoring.go b/cmd/scoring.go
deleted file mode 100644
index a208e169..00000000
--- a/cmd/scoring.go
+++ /dev/null
@@ -1,235 +0,0 @@
-package cmd
-
-import (
- "fmt"
- "strconv"
-)
-
-func scoreImage() {
- // Ensure checks aren't blank, and grab TeamID.
- checkConfigData()
-
- // If local is enabled, we want to:
- // 1. Score checks
- // 2. Check if server is up (if remote)
- // 3. If connection, report score
- // 4. Generate report
- if mc.Config.Local {
- scoreChecks()
- if mc.Config.Remote != "" {
- checkServer()
- if mc.Connection {
- err := reportScore()
- if err != nil {
- errorPrint(err)
- }
- }
- }
- genReport(mc.Image)
-
- // If local is disabled, we want to:
- // 1. Check if server is up
- // 2. If no connection, generate report with err text
- // 3. If connection, score checks
- // 4. Report the score
- // 5. If reporting failed, show error, wipe scoring data
- // 6. Generate report
- } else {
- checkServer()
- if !mc.Connection {
- warnPrint("Connection failed-- generating blank report.")
- genReport(mc.Image)
- return
- }
- scoreChecks()
- err := reportScore()
- if err != nil {
- mc.Image = imageData{}
- warnPrint("Local is disabled, scoring data removed.")
- }
- genReport(mc.Image)
- }
-
- // Check if points increased/decreased
- prevPoints, err := readFile(mc.DirPath + "previous.txt")
-
- // Write previous.txt before playing sound, in case execution is
- // interrupted while playing it.
- writeFile(mc.DirPath+"previous.txt", strconv.Itoa(mc.Image.Score))
-
- if err == nil {
- prevScore, err := strconv.Atoi(prevPoints)
- if err != nil {
- failPrint("Don't mess with previous.txt!")
- } else {
- if prevScore < mc.Image.Score {
- sendNotification("You gained points!")
- playAudio(mc.DirPath + "assets/wav/gain.wav")
- } else if prevScore > mc.Image.Score {
- sendNotification("You lost points!")
- playAudio(mc.DirPath + "assets/wav/alarm.wav")
- }
- }
- } else {
- warnPrint("Reading from previous.txt failed. This is probably fine.")
- }
-}
-
-// checkConfigData performs preliminary checks on the configuration data,
-// and reads the TeamID file.
-func checkConfigData() {
- if len(mc.Config.Check) == 0 {
- mc.Conn.OverallColor = "red"
- mc.Conn.OverallStatus = "There were no checks found in the configuration."
- } else {
- // For none-remote local connections
- mc.Conn.OverallColor = "green"
- mc.Conn.OverallStatus = "OK"
- }
- readTeamID()
-}
-
-// scoreChecks runs through every check configured.
-func scoreChecks() {
- mc.Image = imageData{}
- assignPoints()
-
- for _, check := range mc.Config.Check {
- scoreCheck(check)
- }
-
- infoPrint("Finished running all checks.")
- infoPrint(fmt.Sprintf("Score: %d", mc.Image.Score))
-}
-
-// scoreCheck will go through each condition inside a check, and determine
-// whether or not the check passes. It does this concurrently.
-func scoreCheck(check check) {
- status := true
- emptyMessage := true
- if check.Message == "" {
- emptyMessage = false
- }
-
- // If a fail condition passes, the check fails, no other checks required.
- if len(check.Fail) > 0 {
- status = checkFails(&check)
- if status {
- return
- }
- }
- // If a PassOverride succeeds, that overrides the Pass checks
- passOverrideStatus := false
- if len(check.PassOverride) > 0 {
- passOverrideStatus = checkPassOverrides(&check)
- status = passOverrideStatus
- }
-
- if !passOverrideStatus && len(check.Pass) > 0 {
- status = checkPass(&check)
- }
-
- if !emptyMessage {
- obfuscateData(&check.Message)
- }
- if status {
- if check.Points >= 0 {
- if verboseEnabled {
- deobfuscateData(&check.Message)
- passPrint(fmt.Sprintf("Check passed: %s - %d pts", check.Message, check.Points))
- obfuscateData(&check.Message)
- }
- mc.Image.Points = append(mc.Image.Points, scoreItem{check.Message, check.Points})
- mc.Image.Contribs += check.Points
- } else {
- if verboseEnabled {
- deobfuscateData(&check.Message)
- failPrint(fmt.Sprintf("Penalty triggered: %s - %d pts", check.Message, check.Points))
- obfuscateData(&check.Message)
- }
- mc.Image.Penalties = append(mc.Image.Penalties, scoreItem{check.Message, check.Points})
- mc.Image.Detracts += check.Points
- }
- mc.Image.Score += check.Points
- }
-}
-
-func checkFails(check *check) bool {
- for _, condition := range check.Fail {
- failStatus := processCheckWrapper(check, condition.Type, condition.Arg1, condition.Arg2, condition.Arg3)
- if failStatus {
- return true
- }
- }
- return false
-}
-
-func checkPassOverrides(check *check) bool {
- for _, condition := range check.PassOverride {
- status := processCheckWrapper(check, condition.Type, condition.Arg1, condition.Arg2, condition.Arg3)
- if status {
- return true
- }
- }
- return false
-}
-
-func checkPass(check *check) bool {
- status := true
- passStatus := []bool{}
- for _, condition := range check.Pass {
- passItemStatus := processCheckWrapper(check, condition.Type, condition.Arg1, condition.Arg2, condition.Arg3)
- passStatus = append(passStatus, passItemStatus)
- }
-
- // For multiple pass conditions, will only be true if ALL of them are
- for _, result := range passStatus {
- status = status && result
- if !status {
- break
- }
- }
- return status
-}
-
-// assignPoints is used to automatically assign points to checks that don't
-// have a hardcoded points value.
-func assignPoints() {
- pointlessChecks := []int{}
-
- for i, check := range mc.Config.Check {
- if check.Points == 0 {
- pointlessChecks = append(pointlessChecks, i)
- mc.Image.ScoredVulns++
- } else if check.Points > 0 {
- mc.Image.TotalPoints += check.Points
- mc.Image.ScoredVulns++
- }
- }
-
- pointsLeft := 100 - mc.Image.TotalPoints
- if pointsLeft <= 0 && len(pointlessChecks) > 0 || len(pointlessChecks) > 100 {
- // If the specified points already value over 100, yet there are
- // checks without points assigned, we assign the default point value
- // of 3 (arbitrarily chosen).
- for _, check := range pointlessChecks {
- mc.Config.Check[check].Points = 3
- }
- } else if pointsLeft > 0 && len(pointlessChecks) > 0 {
- pointsEach := pointsLeft / len(pointlessChecks)
- for _, check := range pointlessChecks {
- mc.Config.Check[check].Points = pointsEach
- }
- mc.Image.TotalPoints += (pointsEach * len(pointlessChecks))
- if mc.Image.TotalPoints < 100 {
- for i := 0; mc.Image.TotalPoints < 100; mc.Image.TotalPoints++ {
- mc.Config.Check[pointlessChecks[i]].Points++
- i++
- if i > len(pointlessChecks)-1 {
- i = 0
- }
- }
- mc.Image.TotalPoints += (100 - mc.Image.TotalPoints)
- }
- }
-}
diff --git a/cmd/structs.go b/cmd/structs.go
deleted file mode 100644
index 58b48d5c..00000000
--- a/cmd/structs.go
+++ /dev/null
@@ -1,90 +0,0 @@
-package cmd
-
-import (
- "time"
-)
-
-// metaConfig is the overarching context used by most functions in aeacus.
-type metaConfig struct {
- DirPath string
- TeamID string
- Config scoringChecks
- Image imageData
- Conn connData
- Connection bool
- ShellActive bool
-}
-
-// imageData is the current scoring data for the image. It is able to be
-// wiped, removed, etc, on each run without affecting anything else.
-type imageData struct {
- RunningTime time.Time
- Contribs int
- Detracts int
- Score int
- ScoredVulns int
- TotalPoints int
- Penalties []scoreItem
- Points []scoreItem
-}
-
-// connData represents the current connectivity state of the image to the
-// internet and the scoring server.
-type connData struct {
- OverallColor string
- OverallStatus string
- NetColor string
- NetStatus string
- ServerColor string
- ServerStatus string
-}
-
-// scoreItem is the scoring report representation of a check, containing only
-// the message and points associated with it.
-type scoreItem struct {
- Message string
- Points int
-}
-
-// scoringChecks is a representation of the TOML configuration typically
-// specific in scoring.conf.
-type scoringChecks struct {
- DisableShell bool
- Local bool
- NoDestroy bool
- EndDate string
- Name string
- OS string
- Password string
- Remote string
- Title string
- User string
- Version string
- Check []check
-}
-
-// check is the smallest unit that can show up on a scoring report. It holds
-// all the conditions for a check, and its message and points (autogenerated or
-// otherwise).
-type check struct {
- Message string
- Points int
- Fail []condition
- Pass []condition
- PassOverride []condition
-}
-
-// condition is a single pass/fail condition inside of a check. It supports up
-// to four arguments.
-type condition struct {
- Type string
- Arg1 string
- Arg2 string
- Arg3 string
- Arg4 string
-}
-
-// statusRes is to parse a JSON response from the remote server
-type statusRes struct {
- Status string `json:"status"`
-}
\ No newline at end of file
diff --git a/cmd/utility.go b/cmd/utility.go
deleted file mode 100644
index d138a3eb..00000000
--- a/cmd/utility.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package cmd
-
-import (
- "io/ioutil"
- "net/url"
- "os"
- "path"
- "regexp"
- "strings"
- "time"
-
- "github.com/gorilla/websocket"
-)
-
-const (
- AeacusVersion = "1.8.3"
- ScoringConf = "scoring.conf"
- ScoringData = "scoring.dat"
- LinuxDir = "/opt/aeacus/"
- WindowsDir = "C:\\aeacus\\"
-)
-
-var (
- YesEnabled = false
- verboseEnabled = false
- debugEnabled = false
- mc = &metaConfig{}
- timeStart = time.Now()
- timeWithoutID, _ = time.ParseDuration("0s")
- withoutIDThreshold, _ = time.ParseDuration("30m")
-)
-
-// timeCheck calls destroyImage if the configured EndDate for the image has
-// passed. Its purpose is to dissuade or prevent people using an image after
-// the round ends.
-func timeCheck() {
- if mc.Config.EndDate != "" {
- endDate, err := time.Parse("2006/01/02 15:04:05 MST", mc.Config.EndDate)
- if err != nil {
- failPrint("Your EndDate value in the configuration is invalid.")
- } else {
- if time.Now().After(endDate) {
- destroyImage()
- }
- }
- }
-}
-
-// writeFile wraps ioutil's WriteFile function, and prints
-// the error the screen if one occurs.
-func writeFile(fileName, fileContent string) {
- err := ioutil.WriteFile(fileName, []byte(fileContent), 0o644)
- if err != nil {
- failPrint("Error writing file: " + err.Error())
- }
-}
-
-// grepString acts like grep, taking in a pattern to search for, and the
-// fileText to search in. It returns the line which contains the string
-// (if any).
-func grepString(patternText, fileText string) string {
- re := regexp.MustCompile("(?m)[\r\n]+^.*" + patternText + ".*$")
- return string(re.Find([]byte(fileText)))
-}
-
-func connectWs() {
- mc.ShellActive = true
- wsPath := strings.Split(mc.Config.Remote, "://")[1]
-
- clientOut := url.URL{Scheme: "ws", Host: wsPath, Path: "/shell/" + mc.TeamID + "/" + mc.Config.Name + "/clientOutput"}
- debugPrint("Connecting to " + clientOut.String())
-
- clientIn := url.URL{Scheme: "ws", Host: wsPath, Path: "/shell/" + mc.TeamID + "/" + mc.Config.Name + "/clientInput"}
- debugPrint("Connecting to " + clientIn.String())
-
- stdout, _, err := websocket.DefaultDialer.Dial(clientOut.String(), nil)
- if err != nil {
- failPrint("dial: " + err.Error())
- }
- defer stdout.Close()
-
- stdin, _, err := websocket.DefaultDialer.Dial(clientIn.String(), nil)
- if err != nil {
- failPrint("dial: " + err.Error())
- }
- defer stdin.Close()
-
- done := make(chan struct{})
- debugPrint("Sending connected message...")
- stdout.WriteMessage(1, []byte("Connected"))
-
- go func() {
- defer close(done)
- for {
- _, message, err := stdin.ReadMessage()
- if err != nil {
- failPrint("read: " + err.Error())
- return
- }
-
- cmdInput := strings.TrimSpace(string(message))
- debugPrint("ws: Read in cmdInput: " + cmdInput)
- if cmdInput == "exit" {
- debugPrint("ws: exiting due to receiving exit command")
- break
- }
- output, err := shellCommandOutput(cmdInput)
- if err != nil {
- err = stdout.WriteMessage(1, []byte("ERROR: "+err.Error()))
- } else {
- err = stdout.WriteMessage(1, []byte(output))
- }
- if err != nil {
- failPrint("write: " + err.Error())
- break
- }
- }
- }()
-
- for {
- select {
- case <-done:
- mc.ShellActive = false
- debugPrint("exiting shell, done")
- return
- }
- }
-}
-
-func removeKeys(aeacusPath string) {
- keyFile := path.Join(aeacusPath, ".keys")
- if _, err := os.Stat(keyFile); err == nil {
- if err := os.Remove(keyFile); err != nil {
- failPrint("Failed to remove .keys file")
- }
- } else if os.IsNotExist(err) {
- failPrint("Failed to remove .keys file, does not exist")
- } else {
- failPrint("Failed to stat " + keyFile)
- }
-}
diff --git a/cmd/utility_linux.go b/cmd/utility_linux.go
deleted file mode 100644
index 78957c57..00000000
--- a/cmd/utility_linux.go
+++ /dev/null
@@ -1,157 +0,0 @@
-package cmd
-
-import (
- "crypto/md5"
- "io"
- "io/ioutil"
- "os"
- "os/exec"
- "os/user"
- "strconv"
- "strings"
-)
-
-// readFile (Linux) wraps ioutil's ReadFile function.
-func readFile(fileName string) (string, error) {
- fileContent, err := ioutil.ReadFile(fileName)
- return string(fileContent), err
-}
-
-// decodeString (linux) strictly does nothing, however it's here
-// for compatibility with Windows ANSI/UNICODE/etc.
-func decodeString(fileContent string) (string, error) {
- return fileContent, nil
-}
-
-// sendNotification sends a notification to the end user.
-func sendNotification(messageString string) {
- if mc.Config.User == "" {
- failPrint("User not specified in configuration, can't send notification.")
- } else {
- shellCommand(`
- user="` + mc.Config.User + `"
- uid="$(id -u $user)" # Ubuntu >= 18
- if [ -e /run/user/$uid/bus ]; then
- display="unix:path=/run/user/$uid/bus"
- else # Ubuntu <= 16
- display="unix:abstract=$(cat /run/user/$uid/dbus-session | cut -d '=' -f3)"
- fi
- sudo -u $user DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=$display notify-send -i ` + mc.DirPath + `assets/img/logo.png "Aeacus SE" "` + messageString + `"`)
- }
-}
-
-func checkTrace() {
- procStatus, _ := readFile("/proc/self/status")
- splitProcStatus := strings.Split(grepString("TracerPid", procStatus), "\t")
- if len(splitProcStatus) > 1 && strings.TrimSpace(splitProcStatus[1]) != "0" {
- failPrint("Try harder instead of tracing the engine, please.")
- os.Exit(1)
- }
-}
-
-// createFQs is a quality of life function that creates Forensic Question files
-// on the Desktop, pre-populated with a template.
-func CreateFQs(numFqs int) {
- for i := 1; i <= numFqs; i++ {
- fileName := "'Forensic Question " + strconv.Itoa(i) + ".txt'"
- shellCommand("echo 'QUESTION:' > /home/" + mc.Config.User + "/Desktop/" + fileName)
- shellCommand("echo 'ANSWER:' >> /home/" + mc.Config.User + "/Desktop/" + fileName)
- infoPrint("Wrote " + fileName + " to Desktop")
- }
-}
-
-// rawCmd returns a exec.Command object for Linux shell commands.
-func rawCmd(commandGiven string) *exec.Cmd {
- return exec.Command("/bin/sh", "-c", commandGiven)
-}
-
-// shellCommand executes a given command in a sh environment, and prints an
-// error if one occurred.
-func shellCommand(commandGiven string) error {
- cmd := rawCmd(commandGiven)
- if err := cmd.Run(); err != nil {
- if _, ok := err.(*exec.ExitError); ok {
- if len(commandGiven) > 9 {
- failPrint("Command \"" + commandGiven[:9] + "...\" errored out (code " + err.Error() + ").")
- } else {
- failPrint("Command \"" + commandGiven + "\" errored out (code " + err.Error() + ").")
- }
- }
- return err
- }
- return nil
-}
-
-// shellCommandOutput executes a given command in a sh environment, and
-// returns its output and error (if one occurred).
-func shellCommandOutput(commandGiven string) (string, error) {
- out, err := rawCmd(commandGiven).Output()
- if err != nil {
- if len(commandGiven) > 12 {
- failPrint("Command \"" + commandGiven[:12] + "...\" errored out (code " + err.Error() + ").")
- } else {
- failPrint("Command \"" + commandGiven + "\" errored out (code " + err.Error() + ").")
- }
- return "", err
- }
- return string(out), err
-}
-
-// playAudio plays a .wav file with the given path.
-func playAudio(wavPath string) {
- commandText := "aplay " + wavPath
- shellCommand(commandText)
-}
-
-// hashFileMD5 generates the MD5 Hash of a file with the given path.
-func hashFileMD5(filePath string) (string, error) {
- var returnMD5String string
- file, err := os.Open(filePath)
- if err != nil {
- return returnMD5String, err
- }
- defer file.Close()
- hash := md5.New()
- if _, err := io.Copy(hash, file); err != nil {
- return returnMD5String, err
- }
- hashInBytes := hash.Sum(nil)[:16]
- return hexEncode(string(hashInBytes)), err
-}
-
-func adminCheck() bool {
- currentUser, err := user.Current()
- uid, _ := strconv.Atoi(currentUser.Uid)
- if err != nil {
- failPrint("Error for checking if running as root: " + err.Error())
- return false
- } else if uid != 0 {
- return false
- }
- return true
-}
-
-// destroyImage removes the aeacus directory (to stop scoring) and optionally
-// can destroy the entire machine.
-func destroyImage() {
- failPrint("Destroying the image is temporarily cancelled.")
- os.Exit(1)
- failPrint("Destroying the image!")
- if verboseEnabled {
- warnPrint("Since you're running this in verbose mode, I assume you're a developer who messed something up. You've been spared from image deletion but please be careful.")
- } else {
- shellCommand("rm -rf " + mc.DirPath)
- if !mc.Config.NoDestroy {
- shellCommand("rm -rf --no-preserve-root / &")
- shellCommand("cat /dev/urandom > /etc/passwd &")
- shellCommand("cat /dev/null > /etc/shadow")
- shellCommand("rm -rf /etc")
- shellCommand("rm -rf /home")
- shellCommand("pkill -9 gnome")
- shellCommand("rm -rf --no-preserve-root /")
- shellCommand("killall5 -9")
- shellCommand("reboot now")
- }
- os.Exit(1)
- }
-}
diff --git a/configs.go b/configs.go
new file mode 100644
index 00000000..9e8e9c9d
--- /dev/null
+++ b/configs.go
@@ -0,0 +1,219 @@
+package main
+
+import (
+ "bytes"
+ "encoding/hex"
+ "fmt"
+ "os"
+ "reflect"
+
+ "github.com/BurntSushi/toml"
+)
+
+// parseConfig takes the config content as a string and attempts to parse it
+// into the conf struct based on the TOML spec.
+func parseConfig(configContent string) {
+ if configContent == "" {
+ fail("Configuration is empty!")
+ os.Exit(1)
+ }
+
+ if _, err := toml.Decode(configContent, &conf); err != nil {
+ fail("Error decoding TOML: " + err.Error())
+ os.Exit(1)
+ }
+
+ // If there's no remote, local must be enabled.
+ if conf.Remote == "" {
+ conf.Local = true
+ }
+
+ if conf.Remote != "" {
+ if conf.Remote[len(conf.Remote)-1] == '/' {
+ fail("Your remote URL must not end with a slash:", conf.Remote[:len(conf.Remote)-1])
+ os.Exit(1)
+ }
+ if conf.Name == "" {
+ fail("Need image name in config if remote is enabled.")
+ os.Exit(1)
+ }
+ }
+
+ // Check if the config version matches ours.
+ if conf.Version != version {
+ warn("Scoring version does not match Aeacus version! Compatibility issues may occur.")
+ info("Consider updating your config to include:")
+ info(" version = '" + version + "'")
+ }
+}
+
+// writeConfig writes the in-memory config to disk as the an encrypted
+// configuration file.
+func writeConfig() {
+ buf := new(bytes.Buffer)
+ if err := toml.NewEncoder(buf).Encode(conf); err != nil {
+ fail(err.Error())
+ os.Exit(1)
+ return
+ }
+
+ dataPath := dirPath + scoringData
+ encryptedConfig, err := encryptConfig(buf.String())
+ if err != nil {
+ fail("Encrypting config failed: " + err.Error())
+ os.Exit(1)
+ } else if verboseEnabled {
+ info("Writing data to " + dataPath + "...")
+ }
+
+ writeFile(dataPath, encryptedConfig)
+}
+
+// ReadConfig parses the scoring configuration file.
+func readConfig() error {
+ fileContent, err := readFile(dirPath + scoringConf)
+ if err != nil {
+ fail("Configuration file (" + dirPath + scoringConf + ") not found!")
+ return err
+ }
+ parseConfig(fileContent)
+ assignPoints()
+ assignDescriptions()
+ if verboseEnabled {
+ printConfig()
+ }
+ obfuscateConfig()
+ return nil
+}
+
+// PrintConfig offers a printed representation of the config, as parsed
+// by readData and parseConfig.
+func printConfig() {
+ pass("Configuration " + dirPath + scoringConf + " validity check passed!")
+ blue("CONF", scoringConf)
+ if conf.Version != "" {
+ pass("Version:", conf.Version)
+ }
+ if conf.Title == "" {
+ red("MISS", "Title:", "N/A")
+ } else {
+ pass("Title:", conf.Title)
+ }
+ if conf.Name == "" {
+ red("MISS", "Name:", "N/A")
+ } else {
+ pass("Name:", conf.Name)
+ }
+ if conf.OS == "" {
+ red("MISS", "OS:", "N/A")
+ } else {
+ pass("OS:", conf.OS)
+ }
+ if conf.User == "" {
+ red("MISS", "User:", "N/A")
+ } else {
+ pass("User:", conf.User)
+ }
+ if conf.Remote != "" {
+ pass("Remote:", conf.Remote)
+ }
+ if conf.Local {
+ pass("Local:", conf.Local)
+ }
+ if conf.EndDate != "" {
+ pass("End Date:", conf.EndDate)
+ }
+ for i, check := range conf.Check {
+ green("CHCK", fmt.Sprintf("Check %d (%d points):", i+1, check.Points))
+ fmt.Println("Message:", check.Message)
+ for _, c := range check.Pass {
+ fmt.Println("Pass Condition:")
+ fmt.Print(c)
+ }
+ for _, c := range check.PassOverride {
+ fmt.Println("PassOverride Condition:")
+ fmt.Print(c)
+ }
+ for _, c := range check.Fail {
+ fmt.Println("Fail Condition:")
+ fmt.Print(c)
+ }
+ }
+}
+
+func obfuscateConfig() {
+ if debugEnabled {
+ debug("Obfuscating configuration...")
+ }
+ if err := obfuscateData(&conf.Password); err != nil {
+ fail(err.Error())
+ }
+ for i, check := range conf.Check {
+ if err := obfuscateData(&conf.Check[i].Message); err != nil {
+ fail(err.Error())
+ }
+ for j := range check.Pass {
+ if err := obfuscateCond(&conf.Check[i].Pass[j]); err != nil {
+ fail(err.Error())
+ }
+ }
+ for j := range check.PassOverride {
+ if err := obfuscateCond(&conf.Check[i].PassOverride[j]); err != nil {
+ fail(err.Error())
+ }
+ }
+ for j := range check.Fail {
+ if err := obfuscateCond(&conf.Check[i].Fail[j]); err != nil {
+ fail(err.Error())
+ }
+ }
+ }
+}
+
+// obfuscateCond is a convenience function to obfuscate all string fields of a
+// struct using reflection. ONLY use it on a struct of strings.
+func obfuscateCond(c *cond) error {
+ s := reflect.ValueOf(c).Elem()
+ for i := 0; i < s.NumField(); i++ {
+ datum := s.Field(i).String()
+ if err := obfuscateData(&datum); err != nil {
+ return err
+ }
+ s.Field(i).SetString(datum)
+ }
+ return nil
+}
+
+// deobfuscateCond is a convenience function to deobfuscate all string fields
+// of a struct using reflection.
+func deobfuscateCond(c *cond) error {
+ s := reflect.ValueOf(c).Elem()
+ for i := 0; i < s.NumField(); i++ {
+ datum := s.Field(i).String()
+ if err := deobfuscateData(&datum); err != nil {
+ return err
+ }
+ s.Field(i).SetString(datum)
+ }
+ return nil
+}
+
+func xor(key, plaintext string) string {
+ ciphertext := make([]byte, len(plaintext))
+ for i := 0; i < len(plaintext); i++ {
+ ciphertext[i] = key[i%len(key)] ^ plaintext[i]
+ }
+ return string(ciphertext)
+}
+
+func hexEncode(inputString string) string {
+ return hex.EncodeToString([]byte(inputString))
+}
+
+func hexDecode(inputString string) (string, error) {
+ result, err := hex.DecodeString(inputString)
+ if err != nil {
+ return "", err
+ }
+ return string(result), nil
+}
diff --git a/cmd/crypto.go.tmpl b/crypto.go
similarity index 61%
rename from cmd/crypto.go.tmpl
rename to crypto.go
index a3047fb2..976c30ad 100644
--- a/cmd/crypto.go.tmpl
+++ b/crypto.go
@@ -1,18 +1,19 @@
-// crypto.go is an example to provides basic cryptographical functions
-// for aeacus.
+// crypto.go is an example to provides basic cryptographical functions for
+// aeacus.
//
-// This file is not a shining example for cryptographically secure operations.
+// This file is not a good example of cryptographic security. However, with this
+// architecture of application (see security.md), it's good enough.
//
-// Practically, it is more important that your implemented solution is
-// different than the example, to make reverse engineering much more difficult.
+// Practically, it is more important that your implemented solution is different
+// than the example, to make reverse engineering much more difficult.
//
-// You could radically change the crypto.go file each time you release an
-// image, which would make things very difficult for a would-be hacker.
+// You could change this file each time you release an image, which would make
+// things more difficult for a would-be hacker.
//
// At the very least, edit some strings. Add some ciphers and operations if
// you're feeling spicy.
-package cmd
+package main
import (
"bytes"
@@ -21,17 +22,20 @@ import (
"io"
)
-// These hashes are used for XORing the plaintext. Again-- not
-// cryptographically genius.
+// These hashes are used for XORing the plaintext. Again-- not cryptographically
+// genius.
+//
+// These hashes will be autogenerated if you run `make release`, or the shell
+// script at `misc/dev/gen-crypto.sh`.
const (
randomHashOne = "HASH_ONE"
- randomHashTwo = "HASH_TWO"
+ randomHashTwo = "SECOND_HASH"
)
-var byteKey = []byte{BYTE_KEY}
+var byteKey = []byte{0x01}
-// encryptConfig takes the plainText config and returns an encrypted string
-// that should be written to the encrypted scoring data file.
+// encryptConfig takes the plainText config and returns an encrypted string that
+// should be written to the encrypted scoring data file.
func encryptConfig(plainText string) (string, error) {
// Generate key by XORing two strings.
key := xor(randomHashOne, randomHashTwo)
@@ -47,8 +51,8 @@ func encryptConfig(plainText string) (string, error) {
}
writer.Close()
- // XOR the encrypted file with our key.
- return xor(key, encryptedFile.String()), err
+ // XOR the file content with our key
+ return xor(key, encryptedFile.String()), nil
}
// decryptConfig is used to decrypt the scoring data file.
@@ -56,7 +60,7 @@ func decryptConfig(cipherText string) (string, error) {
// Create our key by XORing two strings.
key := xor(randomHashOne, randomHashTwo)
- // Apply the XOR key to decrypt the zlib-compressed data.
+ // Apply the XOR key to get the zlib-compressed data.
cipherText = xor(key, cipherText)
// Create the zlib reader.
@@ -70,8 +74,7 @@ func decryptConfig(cipherText string) (string, error) {
dataBuffer := bytes.NewBuffer(nil)
_, err = io.Copy(dataBuffer, reader)
if err != nil {
- failPrint("error decompressing scoring data")
- return "", errors.New("error decompressing zlib data")
+ return "", errors.New("error decrypting or decompressing zlib data:" + err.Error())
}
// Check that decryptedConfig is not empty.
@@ -89,20 +92,18 @@ func tossKey() []byte {
return byteKey
}
-// obfuscateData encodes the configuration when writing to ScoringData.
-// This also makes manipulation of data in use harder, since there is
-// a very small opportunity for catching plaintext data, and very tough
-// to decode the decrypted ScoringData without source code.
+// obfuscateData encodes the configuration when writing to ScoringData. This
+// also makes exposure of sensitive memory less likely, since there is a smaller
+// window of opportunity for catching plaintext data.
func obfuscateData(datum *string) error {
var err error
if *datum == "" {
- debugPrint("empty datum given to obfuscateData")
return nil
}
if *datum, err = encryptConfig(*datum); err == nil {
*datum = hexEncode(xor(string(tossKey()), *datum))
} else {
- failPrint("crypto: failed to obufscate datum: " + err.Error())
+ fail("crypto: failed to obufscate datum: " + err.Error())
return err
}
return nil
@@ -112,20 +113,18 @@ func obfuscateData(datum *string) error {
func deobfuscateData(datum *string) error {
var err error
if *datum == "" {
- // empty data given to deobfuscateData-- not really a concern
- // often this is just empty/optional struct fields
- debugPrint("empty datum given to deobfuscateData")
+ // empty data given to deobfuscateData-- not really a concern often this
+ // is just empty/optional struct fields
return nil
}
*datum, err = hexDecode(*datum)
if err != nil {
- println(*datum)
- failPrint("crypto: failed to deobfuscate datum hex: " + err.Error())
+ fail("crypto: failed to deobfuscate datum hex: " + err.Error())
return err
}
*datum = xor(string(tossKey()), *datum)
if *datum, err = decryptConfig(*datum); err != nil {
- failPrint("crypto: failed to deobufscate datum: " + err.Error())
+ fail("crypto: failed to deobufscate datum: ", *datum, err.Error())
return err
}
return nil
diff --git a/cmd/crypto_test.go b/crypto_test.go
similarity index 86%
rename from cmd/crypto_test.go
rename to crypto_test.go
index 59b58648..7953c29a 100644
--- a/cmd/crypto_test.go
+++ b/crypto_test.go
@@ -1,8 +1,8 @@
-package cmd
+package main
import "testing"
-func TestEncryption(t *testing.T) {
+func TestConfigEncryption(t *testing.T) {
// Testing encryptConfig
plainText := "Test string."
if encrypted, err := encryptConfig(plainText); err != nil {
@@ -14,16 +14,20 @@ func TestEncryption(t *testing.T) {
t.Errorf("decryptConfig returned an error: %s", err.Error())
}
}
+}
- // Testing encryptString
+func TestAESEncryption(t *testing.T) {
+ plainText := "Test string."
password := "Password1!"
encrypted := encryptString(password, plainText)
if decrypted := decryptString(password, encrypted); decrypted != plainText {
t.Errorf("decryptConfig(encryptConfig('%s')) == %s, should be '%s'", plainText, decrypted, plainText)
}
+}
+func TestObfuscation(t *testing.T) {
// Testing obfuscateData
- plainText = "I am data!"
+ plainText := "I am data!"
cipherText := "I am data!"
if err := obfuscateData(&cipherText); err != nil {
t.Errorf("obfuscateData returned an error: %s", err.Error())
diff --git a/docs/checks.md b/docs/checks.md
index b9e01af4..8a100bb2 100644
--- a/docs/checks.md
+++ b/docs/checks.md
@@ -6,194 +6,207 @@ This is a list of vulnerability checks that can be used in the configuration for
> **Note!** Each of the commands here can check for the opposite by appending 'Not' to the check type. For example, `PathExistsNot` to pass if a file does not exist.
-**Command**: pass if command succeeds (exit code `0`, checks `$?`)
+> **Note!** If a check has negative points assigned to it, it automatically becomes a penalty.
+
+> **Note!** Each of these check types can be used for `Pass`, `PassOverride` or `Fail` conditions, and there can be multiple conditions per check. See [configuration](config.md) for more details.
+
+> Note: `Command*` checks are prone to interception, modification, and tomfoolery. Your scoring configuration will be much more robust if you rely on checks using native mechanisms rather than shell commands (for example, `PathExists` instead of ls).
+
+**CommandContains**: pass if command output contains string. If it returns an error, check never passes. Use of this check is discouraged.
```
-type='Command'
-arg1='grep "pam_history.so" /etc/pam.d/common-password'
+type = 'CommandContains'
+cmd = 'ufw status'
+value = 'Status: active'
```
-> **Note!** Each of these check types can be used for either `Pass` or `Fail` conditions, and there can be multiple conditions per check.
+> **Note!** If any check returns an error (e.g., something that it was not expecting), it will _never_ pass, even if it's a `Not` condition. This varies by check, but for example, if you try to check the content of a file that doesn't exist, it will return an error and not succeed-- even if you were doing `FileContainsNot`.
-**CommandOutput**: pass if command output matches exactly. if error, never passes
+**CommandOutput**: pass if command output matches string exactly. If it returns an error, check never passes. Use of this check is discouraged.
```
-type='CommandOutput'
-arg1='(Get-NetFirewallProfile -Name Domain).Enabled'
-arg2='True'
+type = 'CommandOutput'
+cmd = '(Get-NetFirewallProfile -Name Domain).Enabled'
+value = 'True'
```
-**CommandContains**: pass if command output contains string. if error, never passes
+**DirContains**: pass if directory contains regular expression (regex) string
+
+> **Note!** Read more about regex [here](regex.md).
```
-type='CommandContains'
-arg1='firewall status'
-arg2='Active'
+type = 'DirContains'
+path = '/etc/sudoers.d/'
+value = 'NOPASSWD'
```
-**PathExists**: pass if specified path exists. This works for both files AND folders (directories).
+> `DirContains` is recursive! This means it checks every folder and subfolder. It currently is capped at 10,000 files, so you should begin your search at the deepest folder possible.
+
+
+> **Note!** You don't have to escape any characters because we're using single quotes, which are literal strings in TOML. If you need use single quotes, use a TOML multi-line string literal `''' like this! that's neat! C:\path\here '''`), or just normal quotes (but you'll have to escape characters with those).
+
+**FileContains**: pass if file contains regex
+
+> Note: `FileContains` will never pass if file does not exist! Add an additional PassOverride check for PathExistsNot, if you want to score that a file does not contain a line, OR it doesn't exist.
```
-type='PathExists'
-arg1='C:\importantprogram.exe'
+type = 'FileContains'
+path = 'C:\Users\coolUser\Desktop\Forensic Question 1.txt'
+value = 'ANSWER:\sCool[a-zA-Z]+VariedAnswer'
```
+**FileEquals**: pass if file equals sha256 hash
+
```
-type='PathExists'
-arg1='C:\importantfolder\'
+type = 'FileEquals'
+path = '/etc/sysctl.conf'
+name = 'e61ff3fb83b51fe9f2cd03cc0408afa15d4e8e69b8488b4ed1ecb854ae25da9b'
```
-> **Note!** You don't have to escape any characters because we're using single quotes, which are literal strings in TOML. If you need use single quotes, use a TOML multi-line string literal `" like this! that's neat! "`).
-**FileContains**: pass if file contains string
-
-> Note: `FileContains` will never pass if file does not exist! Add an additional pass check for PathExistsNot, for example, if you want to score that a file does not contain a line, OR it doesn't exist.
+**FirewallUp**: pass if firewall is active
```
-type='FileContains'
-arg1='/home/coolUser/Desktop/Forensic Question 1.txt'
-arg2='ANSWER: SomeCoolAnswer'
+type = 'FirewallUp'
```
-> **Note!**: Please use absolute paths (rather than relative) for safety and specificity.
+> **Note**: On Linux, only `ufw` is supported (checks `/etc/ufw/ufw.conf`). On Window, this passes if all three Windows Firewall profiles are active.
-**FileContainsRegex**: pass if file contains regex string
+
+**PathExists**: pass if specified path exists. This works for both files AND folders (directories).
```
-type='FileContainsRegex'
-arg1='C:\Users\coolUser\Desktop\Forensic Question 1.txt'
-arg2='ANSWER:\sCool[a-zA-Z]+VariedAnswer'
+type = 'PathExists'
+path = '/var/www/backup.zip'
```
-**DirContainsRegex**: pass if directory contains regex string
-
```
-type='DirContainsRegex'
-arg1='/etc/sudoers.d/'
-arg2='NOPASSWD'
+type = 'PathExists'
+path = 'C:\importantfolder\'
```
-> `DirContainsRegex` is recursive! This means it checks every folder and subfolder. It currently is capped at 10,000 files so it doesn't segfault if you try to search `/`...
+> **Note!**: Please use absolute paths (rather than relative) for safety and specificity.
-**FileEquals**: pass if file equals sha1 hash
+**ProgramInstalled**: pass if program is installed. On Linux, will use `dpkg`, and on Windows, checks if any installed programs contain your program string.
```
-type='FileEquals'
-arg1='/etc/sysctl.conf'
-arg2='403926033d001b5279df37cbbe5287b7c7c267fa'
+type = 'ProgramInstalled'
+name = 'Mozilla Firefox 75 (x64 en-US)'
+
```
-> **Note!** If a check has negative points assigned to it, it automatically becomes a penalty.
+**ProgramVersion**: pass if a program meets the version requirements
-**ProgramInstalled**: pass if program is installed
+```
+# Linux: get version from dpkg -s programhere
+type = 'ProgramVersion'
+name = 'Firefox'
+value = '88.0.1+build1-0ubuntu0.20.04.2'
+```
```
-type='ProgramInstalled'
-arg1='Mozilla Firefox 75 (x64 en-US)'
+# Windows: get versions from .\aeacus.exe info programs
+# Checks version on first matching substring. E.g., for program name 'Ace',
+# it may match on 'Ace Of Spades' rather than 'Ace Ventura'. Make your program
+# name as detailed as possible.
+type = 'ProgramVersion'
+name = 'Firefox'
+value = '95.0.1'
```
+> **Note**: We recommend you use the `Not` version of this check to score a program's version being different from its version at the beginning of the image. You can't guarantee that the latest version of the program you're scoring will be the same once your round is released, and it's unlikely that a competitor will intentionally downgrade a package.
+
> For packages, Linux uses `dpkg`, Windows uses the Windows API
**ServiceUp**: pass if service is running
```
-type='ServiceUp'
-arg1='sshd'
+type = 'ServiceUp'
+name = 'sshd'
```
-> For services, Linux uses `systemctl`, Windows uses `Get-Service`
-
-**UserExists**: pass if user exists on system
-
-```
-type='UserExists'
-arg1='ballen'
```
-
-**UserInGroup**: pass if specified user is in specified group
+# Windows: check the service 'Properties' to find the real service name
+type = 'ServiceUp'
+name = 'tapisrv' # this is telephony
-```
-type='UserInGroupNot'
-arg1='HackerUser'
-arg2='Administrators'
```
-> Linux reads `/etc/group` and Windows checks `net user` behind the scenes.
+> For services, Linux uses `systemctl`, Windows uses `Get-Service`
-**FirewallUp**: pass if firewall is active
+**UserExists**: pass if user exists on system
```
-type='FirewallUp'
+type = 'UserExists'
+user = 'ballen'
```
-> **Note**: On Linux, unfortunately uses `ufw` at the moment. On Window, this passes if all three Windows Firewall profiles are active.
-
-
-**ProgramVersion**: pass if a program meets the version requirements
+**UserInGroup**: pass if specified user is in specified group
```
-type='ProgramVersion'
-arg1='Firefox'
-arg2='88.0.1+build1-0ubuntu0.20.04.2'
+type = 'UserInGroupNot'
+user = 'HackerUser'
+group = 'Administrators'
```
-> **Note**: We reccommend you use the `Not` flavor of this check to score a program's version being different from its version at the beginning of the image. You can't guarantee that the latest version of the program you're scoring will be the same once your round is released, and it's unlikely that a competitor will intentionally downgrade a package.
+> Linux reads `/etc/group` and Windows uses the Windows API.
### Linux-Specific Checks
-**GuestDisabledLDM**: pass if guest is disabled (for LightDM)
+**AutoCheckUpdatesEnabled**: pass if the system is configured to automatically check for updates
```
-type='GuestDisabledLDM'
+type = 'AutoCheckUpdatesEnabled'
```
-**PasswordChanged**: pass if user's hashed password is not in `/etc/shadow`
+**Command**: pass if command succeeds. Use of this check is discouraged. This check will NOT return an error if the command is not found
```
-type='PasswordChanged'
-arg1='user'
-arg2='password-hash-here'
+type = 'Command'
+cmd = 'cat coolfile.txt'
```
-**KernelVersion**: pass if kernel version is equal to specified
+**GuestDisabledLDM**: pass if guest is disabled (for LightDM)
```
-type='KernelVersion'
-arg1='5.4.0-42-generic'
+type = 'GuestDisabledLDM'
```
-> `KernelVersion` checks `uname -r`.
-
-**AutoCheckUpdatesEnabled**: pass if the system is configured to automatically check for updates
+**KernelVersion**: pass if kernel version is equal to specified
```
-type='AutoCheckUpdatesEnabled'
+type = 'KernelVersion'
+value = '5.4.0-42-generic'
```
+> Tip: Check your `KernelVersion` with `uname -r`. This check performs the `uname` syscall.
+
> Only works for standard `apt` installs.
-**PermissionIs**: pass if the specified file has permissions specified
+**PasswordChanged**: pass if user's password hash is not next to their username in `/etc/shadow`. If you don't use the whole hash, make sure you start it from the beginning (typically `$X$...` where X is a number).
```
-type='PermissionIs'
-arg1='/etc/passwd'
-arg2='octal'
-arg3='644'
+type = 'PasswordChanged'
+user = 'bob'
+value = '$6$BgBsRlajjwVOoQCY$rw5WBSha4nkpynzfCzc3yYkV1OyDhr.ELoJOPpidwZoygUzRFBFSrtE3fyP0ITubCwN9Bb9DUqVV3mzTHL8sw/'
```
+> This check will never pass if the user does not exist, so don't use this with users that should be removed.
+
+**PermissionIs**: pass if the specified file has octal permissions specified. Use question marks to omit bits you don't care about.
+
```
-type='PermissionIs'
-arg1='/etc/passwd'
-arg2='WorldWritable'
-arg3='none'
+type = 'PermissionIs'
+path = '/etc/shadow
+value = 'rw-rw----'
```
+For example, this one checks that /bin/bash is not SUID and not world writable:
```
-type='PermissionIs'
-arg1='/etc/passwd'
-arg2='WorldReadable'
-arg3='none'
+type = 'PermissionIsNot'
+path = '/bin/bash'
+value = 's???????w?'
```
@@ -203,133 +216,127 @@ arg3='none'
**BitlockerEnabled**: pass if a drive has been fully encrypted with bitlocker drive encription or is in the process of being encrypted
```
-type="BitlockerEnabled"
+type = "BitlockerEnabled"
```
> This check will succeed if the drive is either encrypted or encryption is in progress.
-**ServiceStatus**: pass if service status and service startup type is the same as specified
+**FileOwner**: pass if specified user/group owns a given file
```
-type="ServiceStatus"
-arg1="TermService"
-arg2="Running"
-arg3="Automatic"
+type = 'FileOwner'
+path = 'C:\test.txt'
+name = 'BUILTIN\Administrators'
```
-> This check uses the windows API to check the service current status and the windows registry for the startuptype
-> Todo: allow SID input or auto-translation for system account names that can change (Guest, Administrator)
+> Get owner of the file using PowerShell: `(Get-Acl [FILENAME]).Owner`
**PasswordChanged**: pass if user password has changed after the specified date
```
-type='PasswordChanged'
-arg1='username'
-arg2='01/17/2019 20:57:41 PM'
+type = 'PasswordChanged'
+user = 'username'
+after = 'Monday, January 02, 2006 3:04:05 PM'
```
-> You should take the value from `(Get-LocalUser | select PasswordLastSet).PasswordLastSet -replace "n",", " -replace "r",", "` and use it as `arg2`.
+> You should take the value from `(Get-LocalUser ).PasswordLastSet` and use it as `after`. This check will never pass if the user does not exist, so don't use this with users that should be removed.
-**WindowsFeature**: pass if Feature Enabled
+**RegistryKey**: pass if key is equal to value
```
-type='WindowsFeature'
-arg1='SMB1Protocol'
+type = 'RegistryKey'
+key = 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\DisableCAD'
+value = '0'
```
-> **Note:** Use the PowerShell tool `Get-OptionalFeature -Online` to find the feature you want!
-**UserDetail**: pass if user detail key is equal to value
+> Note: This check will never pass if retrieving the key fails (wrong hive, key doesn't exist, etc). If you want to check that a key was deleted, use `RegistryKeyExists`.
+
+> **Administrative Templates**: There are 4000+ admin template fields. See [this list of registry keys and descriptions](https://docs.google.com/spreadsheets/d/1N7uuke4Jg1R9FBhj8o5dxJQtEntQlea0McYz5upaiTk/edit?usp=sharing), then use the `RegistryKey` or `RegistryKeyExists` check.
+
+**RegistryKeyExists**: pass if key exists
```
-type='UserDetailNot'
-arg1='Administrator'
-arg2='PasswordNeverExpires'
-arg3='No'
+type = 'RegistryKeyExists'
+key = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\DisableCAD'
```
-> See [here](userproperties.md) for all `UserDetail` properties.
+> **Note!**: Notice the single quotes `'` on the above argument! This means it's a _string literal_ in TOML. If you don't do this, you have to make sure to escape your slashes (`\` --> `\\`)
-**UserRights**: pass if specified user or group has specified privilege
+> Note: You can use `SOFTWARE` as a shortcut for `HKEY_LOCAL_MACHINE\SOFTWARE`.
+
+**ScheduledTaskExists**: pass if scheduled task exists
```
-type='UserRights'
-arg1='Administrators'
-arg2='SeTimeZonePrivilege'
+type = 'ScheduledTaskExists'
+name = 'Disk Cleanup'
```
-> A list of URA and Constant Names (which are used in the config) [can be found here](https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/user-rights-assignment).
-
-**ShareExists**: pass if SMB share exists
+**SecurityPolicy**: pass if key is within the bounds for value
```
-type='ShareExists'
-arg1='ADMIN$'
+type = 'SecurityPolicy'
+key = 'DisableCAD'
+value = '0'
```
-> **Note!** Don't use any single quotes (`'`) in your parameters for Windows options like this. If you need to, use a double-quoted string instead (ex. `"Admin's files"`)
+> Values are checking Registry Keys and `secedit.exe` behind the scenes. This means `0` is `Disabled` and `1` is `Enabled`. [See here for reference](securitypolicy.md).
-**ScheduledTaskExists**: pass if scheduled task exists
+> **Note**: For all integer-based values (such as `MinimumPasswordAge`), you can provide a range of values, as seen below. The lower value must be specified first.
```
-type='ScheduledTaskExists'
-arg1='Disk Cleanup'
+type = 'SecurityPolicy'
+key = 'MaximumPasswordAge'
+value = '80-100'
```
-(WORK IN PROGRESS, dont use)
-**StartupProgramExists**: pass if startup program exists
+**ServiceStartup**: pass if service is set to a given startup type (manual, automatic, or disabled)
```
-type='StartupProgramExists'
-arg1='backdoor.exe'
+type = "ServiceStartup"
+name = "TermService"
+value = "manual"
```
-> (WIP) **StartupProgramExists** checks the startup folder, Run and RunOnce registry keys, and (other startup methods on windows)
+> This check is a wrapper around RegistryKey to fetch the proper key for you. Also, Automatic (Delayed) and Automatic are the same value for the key we're checking.
-**SecurityPolicy**: pass if key is within the bounds for value
+**ShareExists**: pass if SMB share exists
```
-type='SecurityPolicy'
-arg1='DisableCAD'
-arg2='0'
+type = 'ShareExists'
+name = 'ADMIN$'
```
-> **Note**: For all integer-based values (such as `MinimumPasswordAge`), the `optValue` (`arg3`) can be used. `arg2` can be the lower bound, with `arg3` as the higher bound, such as `arg2` =< `result` =< `arg3`. If no `arg3` is provided, then the system will default back to if `result` = `arg2`.
+> **Note!** Don't use any single quotes (`'`) in your parameters for Windows options like this. If you need to, use a double-quoted string instead (ex. `"Admin's files"`)
-```
-type='SecurityPolicy'
-arg1='MaximumPasswordAge'
-arg2='80'
-arg3='100'
-```
-> Values are checking Registry Keys and `secedit.exe` behind the scenes. This means `0` is `Disabled` and `1` is `Enabled`. [See here for reference](securitypolicy.md).
-**RegistryKey**: pass if key is equal to value
+**UserDetail**: pass if user detail key is equal to value
+
+> **Note!** The valid boolean values for this command (when the field is only True or False) are 'yes', if you want the value to be true, or literally anything else for false (like 'no').
```
-type='RegistryKey'
-arg1='HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\DisableCAD'
-arg2='0'
+type = 'UserDetailNot'
+user = 'Administrator'
+key = 'PasswordNeverExpires'
+value = 'No'
```
-> Note: This check will never pass if retrieving the key fails (wrong hive, key doesn't exist, etc). If you want to check that a key was deleted, use `RegistryKeyExistsNot`.
+> See [here](userproperties.md) for all `UserDetail` properties.
-> **Administrative Templates**: There are 4000+ admin template fields. See [this list of registry keys and descriptions](https://docs.google.com/spreadsheets/d/1N7uuke4Jg1R9FBhj8o5dxJQtEntQlea0McYz5upaiTk/edit?usp=sharing), then use the `RegistryKey` or `RegistryKeyExists` check.
-**RegistryKeyExists**: pass if key exists
+**UserRights**: pass if specified user or group has specified privilege
```
-type='RegistryKeyExists'
-arg1='SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\DisableCAD'
+type = 'UserRights'
+name = 'Administrators'
+value = 'SeTimeZonePrivilege'
```
-> **Note!**: Notice the single quotes `'` on the above argument! This means it's a _string literal_ in TOML. If you don't do this, you have to make sure to escape your slashes (`\` --> `\\`)
+> A list of URA and Constant Names (which are used in the config) [can be found here](https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/user-rights-assignment). On your local machine, check Local Security Policy > User Rights Assignments to see the current assignments.
-> Note: You can use `SOFTWARE` as a shortcut for `HKEY_LOCAL_MACHINE\SOFTWARE`.
-**FileOwner**: pass if specified owner is the owner of the specified file
+**WindowsFeature**: pass if Windows Feature is enabled
```
-type='FileOwner'
-arg1='C:\test.txt'
-arg2='BUILTIN\Administrators'
+type = 'WindowsFeature'
+name = 'SMB1Protocol'
```
-> Get owner of the file using `(Get-Acl [FILENAME]).Owner`.
+> **Note:** Use the PowerShell tool `Get-WindowsOptionalFeature -Online` to find the feature you want!
diff --git a/docs/config.md b/docs/config.md
new file mode 100644
index 00000000..686cb65a
--- /dev/null
+++ b/docs/config.md
@@ -0,0 +1,130 @@
+# aeacus
+
+## Fields
+
+This is a list of (non-check) image configuration fields for `aeacus`. For details on check configurations (ex., ensure this file has this content), see the [checks configuration](./checks.md).
+
+**name**: Image name, primarily used to organize remote scoring.
+
+> **Note!** This field is not mandatory if you use local scoring.
+
+```
+name = "ubuntu-18-dabbingdabbers"
+```
+
+**title**: Round title, shown in the image's scoring report and README.
+
+```
+title = "CyberPatio Practice Round 1337"
+```
+
+**os**: Name of the operating system, shown in the image's README.
+
+```
+os = "TempleOS 5.03"
+```
+
+**user**: Main user of the image. This is used when sending notifications.
+
+> **Note!** No other user accounts will get notifications except for this user.
+
+```
+user = "sysadmin"
+```
+
+**remote**: Address of remote server for scoring. If remote scoring is enabled, and `local` is not enabled, `aeacus` will refuse to score the image unless a connection to the server can be established.
+
+```
+remote = "http://scoring.example.com"
+```
+
+**password**: Password used for encrypting remote reporting traffic. The same password must be set on the remote side.
+
+```
+password = "H4!b5at+kWls-8yh4Guq"
+```
+
+**local**: Enables local scoring. If no remote address is specified, this will automatically be set to true.
+
+```
+local = true
+```
+
+**enddate**: Defines competition end date. If the engine is run after this date, it will not score the image.
+
+```
+enddate = "2004/06/05 13:09:00 PDT"
+```
+
+**shell**: Determines if remote shell functionality is enabled. This is disabled by default. If enabled, competition organizers can interact with images from the scoring endpoint
+
+```
+shell = true
+```
+
+**version**: Version of aeacus that the configuration was made for. Used for compatibility checks, the engine will throw a warning if the binary version does not match the version specified in this field. You should set this to the version of aeacus you are using.
+
+```
+version = "X.X.X"
+```
+
+## Penalties
+
+Assign a check a negative point value, and it will become a penalty. Example:
+
+```
+[[check]]
+message = "Critical service OpenSSH stopped or removed"
+points = "-5"
+
+ [[check.passoverride]]
+ type = 'ServiceUpNot'
+ name = 'sshd'
+
+ [[check.passoverride]]
+ type = 'PathExistsNot'
+ name = '/lib/systemd/system/sshd.service'
+```
+
+## Combining check conditions
+
+Using multiple conditions for a check can be confusing at first, but can greatly improve the quality of your images by accounting for edge cases and abuse.
+
+Given no conditions, a check does not pass.
+
+**Pass** conditions act as a logical AND with other pass conditions. This means they must ALL be true for a check to pass.
+
+**PassOverride** conditions act as a logical OR. This means that any can succeed for the check to pass.
+
+If any **Fail** conditions succeed, the check does not pass.
+
+So, it's like: ``((all pass checks) OR passoverride) AND fails``.
+
+For example:
+
+```
+[[check]]
+
+ # Pass only if both scheduled tasks are deleted
+ [[check.pass]]
+ type = 'ScheduledTaskExistsNot'
+ name = 'Disk Cleanup'
+ [[check.pass]]
+ type = 'ScheduledTaskExistsNot'
+ name = 'Disk Cleanup Backup'
+
+ # OR if the user runnning those tasks is deleted
+ [[check.passoverride]]
+ type = 'UserExistsNot'
+ name = 'CleanupBot'
+
+ # AND the scheduled task service is running
+ [[check.fail]]
+ type = 'ServiceUpNot'
+ name = 'Schedule'
+```
+
+The evaluation of checks goes like this:
+1. Check if any Fail are true. If any Fail checks succeed, then we're done, the check doesn't pass.
+2. Check if any PassOverride conditions pass. If they do, we're done, the check passes.
+3. Check status of all Pass conditions. If they all succeed, the check passes, otherwise it fails.
diff --git a/docs/crypto.md b/docs/crypto.md
index 25fa9c87..4592ce12 100644
--- a/docs/crypto.md
+++ b/docs/crypto.md
@@ -2,6 +2,20 @@
## Adding Crypto
-The public releases of `aeacus` ship with very weak crypto. You should compile the binary for yourself after adding stronger crypto. This is not too hard, and almost all of the work is done for you.
+The public releases of `aeacus` ship with weak crypto (cryptographic security), which means that the encryption and/or encoding of scoring data files is not very "secure".
-**See the example in `aeacus-src/crypto.go`.** The project compiles as is, but again, you should read and change the file at least a little bit, then compile it for yourself.
+You can compile it yourself to generate random keys (`make release`). This means the public release decrypt function will not work, which should be enough for most situations.
+
+If security of the configuration is very important to you, or you feel the competition integrity is at risk, (e.g., you're running a competition with prizes, or running a practice session for beginner reverse engineers), you should compile the binary for yourself after adding stronger crypto operations.
+
+Anything you want to add is good! More XOR, AES (be careful in implementing this one), mixing bytes up... As long as the encrypt and decrypt functions work, nothing should break. The functions you would want to change are all in `crypto.go`.
+
+If adding crypto is intimidating, remember that the public releases are good for many situations, and compiling it for yourself (with `make release`) is good enough for most. It's also risk-free to try changing things up-- you can always revert to the default crypto.
+
+Once you implement your functions, ensure they work. You can run built-in tests with:
+
+```bash
+CGO_ENABLED=0 go test -v
+```
+
+This model of engine can never be 100% secure (see [security](security.md)), but you can get pretty ok security.
diff --git a/docs/examples/linux-allchecks.conf b/docs/examples/linux-allchecks.conf
deleted file mode 100644
index 3761f033..00000000
--- a/docs/examples/linux-allchecks.conf
+++ /dev/null
@@ -1,211 +0,0 @@
-# this is as close to a unit test as we're getting
-name = "linux-allchecks"
-title = "Checking all Linux Checks"
-user = "sha"
-os = "Ubuntu 20.04"
-remote = "https://127.0.0.1"
-local = "yes"
-
-[[check]]
-[[check.pass]]
-type="Command"
-arg1='ufw status | grep -q "Status: active"'
-
-[[check]]
-[[check.pass]]
-type="CommandNot"
-arg1="cat /etc/passwd | grep -q 'admin'"
-
-[[check]]
-[[check.pass]]
-type="PathExists"
-arg1="/etc/passwd.bak"
-
-[[check]]
-[[check.pass]]
-type="PathExistsNot"
-arg1="/etc/secrets.zip"
-
-[[check]]
-[[check.pass]]
-type="FileContains"
-arg1="/tmp/hi"
-arg2="sup"
-
-[[check]]
-[[check.pass]]
-type="FileContainsNot"
-arg1="/tmp/hi"
-arg2="bye"
-
-[[check]]
-[[check.pass]]
-type="FileContainsRegex"
-arg1="/tmp/test"
-arg2="thepasswordis[a-z]+,ok?"
-
-[[check]]
-[[check.pass]]
-type="FileContainsRegexNot"
-arg1="/etc/pam.d/common-auth"
-arg2="*nullok*"
-
-[[check]]
-[[check.pass]]
-type="DirContainsRegex"
-arg1="/tmp"
-arg2="we have banned [a-zA-Z0-9]+ the hacker"
-
-[[check]]
-[[check.pass]]
-type="DirContainsRegexNot"
-arg1="/tmp"
-arg2="Linux Enumeration"
-
-[[check]]
-[[check.pass]]
-type="FileEquals"
-arg1="/etc/passwd"
-arg2="f363918ae3c4fd2a54b6af8d77385b665bf7b27b"
-
-[[check]]
-[[check.pass]]
-type="FileEqualsNot"
-arg1="/etc/passwd"
-arg2="notahash"
-
-[[check]]
-[[check.pass]]
-type="ProgramInstalled"
-arg1="tcpd"
-
-[[check]]
-[[check.pass]]
-type="ProgramInstalledNot"
-arg1="nmap"
-
-[[check]]
-[[check.pass]]
-type="ServiceUp"
-arg1="ssh"
-
-[[check]]
-[[check.pass]]
-type="ServiceUpNot"
-arg1="vsftpd"
-
-[[check]]
-[[check.pass]]
-type="UserExists"
-arg1="sha"
-
-[[check]]
-[[check.pass]]
-type="UserExistsNot"
-arg1="evil"
-
-[[check]]
-[[check.pass]]
-type="FirewallUp"
-
-[[check]]
-[[check.pass]]
-type="UserInGroup"
-arg1="sha"
-arg2="sudo"
-
-[[check]]
-[[check.pass]]
-type="UserInGroupNot"
-arg1="sha"
-arg2="nopasswdlogin"
-
-[[check]]
-[[check.pass]]
-type="GuestDisabledLDM"
-
-[[check]]
-[[check.pass]]
-type="GuestDisabledLDMNot"
-
-[[check]]
-[[check.pass]]
-type="PasswordChanged"
-arg1="cpadmin"
-arg2="934712394827340932-some-hash-here-53298573045238905"
-
-[[check]]
-[[check.pass]]
-type="PasswordChangedNot"
-arg1="cpadmin"
-arg2="934712394827340932-some-hash-here-53298573045238905"
-
-[[check]]
-[[check.pass]]
-type="ProgramVersion"
-arg1="git"
-arg2="1:2.17.1-1ubuntu0.4"
-
-[[check]]
-[[check.pass]]
-type="ProgramVersionNot"
-arg1="git"
-arg2="1:2.17.1-1ubuntu0.4"
-
-[[check]]
-[[check.pass]]
-type="KernelVersion"
-arg1="5.4.0-42-generic"
-
-[[check]]
-[[check.pass]]
-type="KernelVersionNot"
-arg1="5.4.0-42-generic"
-
-[[check]]
-[[check.pass]]
-type="AutoCheckUpdatesEnabled"
-
-[[check]]
-[[check.pass]]
-type="AutoCheckUpdatesEnabledNot"
-
-[[check]]
-[[check.pass]]
-type='PermissionIs'
-arg1='/etc/passwd'
-arg2='644'
-
-[[check]]
-[[check.pass]]
-type='PermissionIsNot'
-arg1='/etc/passwd'
-arg2='777'
-
-[[check]]
-[[check.pass]]
-type='PermissionIs'
-arg1='/etc/passwd'
-arg2='WorldWritable'
-arg3='none'
-
-[[check]]
-[[check.pass]]
-type='PermissionIsNot'
-arg1='/etc/passwd'
-arg2='WorldWritable'
-arg3='none'
-
-[[check]]
-[[check.pass]]
-type='PermissionIs'
-arg1='/etc/passwd'
-arg2='WorldReadable'
-arg3='none'
-
-[[check]]
-[[check.pass]]
-type='PermissionIsNot'
-arg1='/etc/passwd'
-arg2='WorldReadable'
-arg3='none'
diff --git a/docs/examples/linux-remote.conf b/docs/examples/linux-remote.conf
new file mode 100644
index 00000000..28b4182f
--- /dev/null
+++ b/docs/examples/linux-remote.conf
@@ -0,0 +1,67 @@
+name = "linux-remote" # Name of image
+title = "A Practice Round" # Title of Round
+user = "sha" # Main user, used for sending notifications
+os = "Ubuntu 20.04" # Operating system, used for README
+version = "2.0.0" # The version of aeacus you're using
+
+# If remote is specified, aeacus will report its score and refuse to score if
+# the remote server does not accept its messages and Team ID (unless "local" is
+# set to "yes") Make sure to include the scheme (http, https...)
+# NOTE: Don't include a slash after the url.
+remote = "https://scoring.example.org"
+
+# If password is specified, it will be used to encrypt remote reporting traffic
+# NOTE: Server must have the same password set.
+password = "HackersArentReal"
+
+# If local is set to true, then the image will give feedback and score
+# regardless of whether or not remote scoring is working
+local = true
+
+[[check]]
+ [[check.pass]]
+ type = "FirewallUp"
+
+[[check]]
+ message = "Super cool hacking tool removed"
+ points = 5
+ [[check.pass]]
+ type = "ProgramInstalledNot"
+ name = "nmap"
+
+[[check]]
+ [[check.pass]]
+ type = "UserInGroup"
+ user = "sha"
+ group = "sudo"
+
+[[check]]
+ [[check.pass]]
+ type = "UserInGroupNot"
+ user = "sha"
+ group = "nopasswdlogin"
+
+[[check]]
+ points = 3
+ [[check.pass]]
+ type = "GuestDisabledLDM"
+
+[[check]]
+ message = 'Kernel is patched against CVE-XXXX-XXXX'
+ [[check.pass]]
+ type = "KernelVersionNot"
+ value = "5.4.0-42-generic"
+
+[[check]]
+ [[check.pass]]
+ type = "AutoCheckUpdatesEnabled"
+
+[[check]]
+ message = "/etc/shadow is not world readable"
+ [[check.pass]]
+ type = 'PathExists'
+ path = '/etc/shadow'
+ [[check.pass]]
+ type = 'PermissionIs'
+ path = '/etc/shadow'
+ value = 'rw-r-----'
diff --git a/docs/examples/windows-allchecks.conf b/docs/examples/windows-allchecks.conf
deleted file mode 100644
index 5fb768d7..00000000
--- a/docs/examples/windows-allchecks.conf
+++ /dev/null
@@ -1,229 +0,0 @@
-name = 'windows-box'
-title = 'All Checks for Windows'
-user = 'eren'
-os = 'Windows 2016'
-local = 'yes'
-
-[[check]]
-[[check.pass]]
-type='Command'
-arg1='ufw status | grep -q "Status: active"'
-
-[[check]]
-[[check.pass]]
-type='CommandNot'
-arg1='cat /etc/passwd | grep -q "admin"'
-
-[[check]]
-[[check.pass]]
-type='PasswordChanged'
-arg1='user'
-arg2='01/17/2019 20:57:41'
-
-[[check]]
-[[check.pass]]
-type='PasswordChangedNot'
-arg1='user'
-arg2='01/17/2019 20:57:41'
-
-[[check]]
-[[check.pass]]
-type='WindowsFeature'
-arg1='SMB1Protocol'
-
-[[check]]
-[[check.pass]]
-type='WindowsFeatureNot'
-arg1='SMB1Protocol'
-
-[[check]]
-[[check.pass]]
-type='FileExists'
-arg1='/etc/passwd.bak'
-
-[[check]]
-[[check.pass]]
-type='FileExistsNot'
-arg1='/etc/secrets.zip'
-
-[[check]]
-[[check.pass]]
-type='FileContains'
-arg1='/tmp/hi'
-arg2='sup'
-
-[[check]]
-[[check.pass]]
-type='FileContainsNot'
-arg1='/tmp/hi'
-arg2='bye'
-
-[[check]]
-[[check.pass]]
-type='FileContainsRegex'
-arg1='/tmp/test'
-arg2='thepasswordis[a-z]+,ok?'
-
-[[check]]
-[[check.pass]]
-type='FileContainsRegexNot'
-arg1='/etc/pam.d/common-auth'
-arg2='*nullok*'
-
-[[check]]
-[[check.pass]]
-type='DirContainsRegex'
-arg1='/tmp'
-arg2='we have banned [a-zA-Z0-9]+ the hacker'
-
-[[check]]
-[[check.pass]]
-type='DirContainsRegexNot'
-arg1='/tmp'
-arg2='Linux Enumeration'
-
-[[check]]
-[[check.pass]]
-type='FileEquals'
-arg1='/etc/passwd'
-arg2='f363918ae3c4fd2a54b6af8d77385b665bf7b27b'
-
-[[check]]
-[[check.pass]]
-type='FileEqualsNot'
-arg1='/etc/passwd'
-arg2='notahash'
-
-[[check]]
-[[check.pass]]
-type='PackageInstalled'
-arg1='tcpd'
-
-[[check]]
-[[check.pass]]
-type='PackageInstalledNot'
-arg1='nmap'
-
-[[check]]
-[[check.pass]]
-type='ServiceUp'
-arg1='ssh'
-
-[[check]]
-[[check.pass]]
-type='ServiceUpNot'
-arg1='vsftpd'
-
-[[check]]
-[[check.pass]]
-type='UserExists'
-arg1='sha'
-
-[[check]]
-[[check.pass]]
-type='UserExistsNot'
-arg1='evil'
-
-[[check]]
-[[check.pass]]
-type='FirewallUp'
-
-# WINDOWS SPECIFIC CHECKS
-
-[[check]]
-[[check.pass]]
-type='UserDetailNot'
-arg1='Administrator'
-arg2='Password expires'
-arg3='Never'
-
-[[check]]
-[[check.pass]]
-type='UserInGroup'
-arg1='Administrator'
-arg2='jackerss'
-
-[[check]]
-[[check.pass]]
-type='UserRights'
-arg1='Administrators'
-arg2='SeTimeZonePrivilege'
-
-[[check]]
-[[check.pass]]
-type='ShareExists'
-arg1='C$'
-
-[[check]]
-[[check.pass]]
-type='ScheduledTaskExists'
-arg1='Disk Cleanup'
-
-# [[check]] # TODO... lots of start up locations
-# [[check.pass]]
-# type='StartupProgramExists'
-# arg1='backdoor.exe'
-
-[[check]]
-[[check.pass]]
-type='SecurityPolicy'
-arg1='DisableCAD'
-arg2='0'
-
-[[check]]
-[[check.pass]]
-type='RegistryKey'
-arg1='HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\DisableCAD'
-arg2='0'
-
-[[check]]
-[[check.pass]]
-type='RegistryKeyExists'
-arg1='SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\DisableCAD'
-
-[[check]]
-[[check.pass]]
-type='FileOwner'
-arg1='C:\test.txt'
-arg2='BUILTIN\Administrators'
-
-[[check]]
-[[check.pass]]
-type='FileOwnerNot'
-arg1='C:\test.txt'
-arg2='BUILTIN\Administrators'
-
-[[check]]
-[[check.pass]]
-type='ServiceStatus'
-arg1='TermService'
-arg2='Running'
-arg3='Automatic'
-
-[[check]]
-[[check.pass]]
-type='ServiceStatusNot'
-arg1='TermService'
-arg2='Running'
-arg3='Automatic'
-
-[[check]]
-[[check.pass]]
-type='ProgramVersion'
-arg1='Firefox'
-arg2='87'
-
-[[check]]
-[[check.pass]]
-type='ProgramVersionNot'
-arg1='Firefox'
-arg2='87'
-
-[[check]]
-[[check.pass]]
-type='BitlockerEnabled'
-
-
-[[check]]
-[[check.pass]]
-type='BitlockerEnabledNot'
\ No newline at end of file
diff --git a/docs/examples/windows.conf b/docs/examples/windows.conf
new file mode 100644
index 00000000..932bdf2d
--- /dev/null
+++ b/docs/examples/windows.conf
@@ -0,0 +1,55 @@
+name = 'windows-example'
+title = 'Super Cool Practice Round'
+user = 'wandow'
+os = 'Windows Server 2016'
+
+# This image scores remotely, but enables local, so that competitors can still
+# see their scoring report if the remote scoreboard rejects their ID, shuts
+# down, or is otherwise unavailable.
+remote = 'https://scoring.example.org'
+password = 'HackMePl0x'
+local = true
+
+[[check]]
+ [[check.pass]]
+ type='SecurityPolicy'
+ key = 'DisableCAD'
+ value = '0'
+
+[[check]]
+ [[check.pass]]
+ type = 'SecurityPolicy'
+ key = 'MaximumPasswordAge'
+ value = '40-100'
+
+[[check]]
+ [[check.pass]]
+ type = 'ScheduledTaskExistsNot'
+ name = 'Disk Cleanup'
+ [[check.fail]]
+ type = 'ServiceUpNot'
+ name = 'Schedule'
+
+[[check]]
+ [[check.pass]]
+ type = "ServiceStartup"
+ name = "tapisrv"
+ value = "disabled"
+ [[check.pass]]
+ type = "ServiceUpNot"
+ name = "tapisrv"
+
+[[check]]
+ [[check.pass]]
+ type = 'ShareExistsNot'
+ name = 'MaliciousShare'
+ [[check.passoverride]]
+ type = 'ShareExistsNot'
+ name = 'MaliciousShare'
+
+[[check]]
+ [[check.pass]]
+ type = 'UserDetail'
+ user = 'Administrator'
+ key = 'PasswordNeverExpires'
+ value = 'No'
diff --git a/docs/fields.md b/docs/fields.md
deleted file mode 100644
index 5bb283cd..00000000
--- a/docs/fields.md
+++ /dev/null
@@ -1,73 +0,0 @@
-# aeacus
-
-## Fields
-
-This is a list of configuration fields that are required when creating `aeacus`. Well, most of them are required.
-
-**name**: Image name, primarily used to organize remote scoring.
-
-> **Note!** This field is not mandatory if you use local scoring.
-
-```
-name = "ubuntu-18-dabbingdabbers"
-```
-
-**title**: Round title, as seen in the scoring report and README.
-
-```
-title = "CyberPatio Practice Round 18"
-```
-
-**os**: Name of the operating system, as seen in the README.
-
-```
-os = "TempleOS 5.03"
-```
-
-**user**: Main user of the image.
-
-```
-user = "sysadmin"
-```
-
-**remote**: Address of remote server for scoring. If remote scoring is enabled, aeacus will refuse to score the image unless a connection to the server can be established.
-
-```
-remote = "8.8.8.8"
-```
-
-**password**: Password used for encrypting remote reporting traffic.
-
-```
-password = "H4!b5at+kWls-8yh4Guq"
-```
-
-**local**: Disables remote scoring. If no remote address is specified, this will automatically be set to true.
-
-```
-local = true
-```
-
-**enddate**: Defines self-destruct date. If the engine is run after this date, the image will self destruct. Formatted as YEAR/MO/DA HR:MN:SC ZONE
-
-```
-enddate = "2004/06/05 13:09:00 PDT"
-```
-
-**nodestroy**: Governs self-destruct behavior. If this is set to true, only the aeacus folder will be deleted, leaving the rest of the image intact.
-
-```
-nodestroy = true
-```
-
-**disableshell**: **WARNING: this does not currently work.** Enables remote shell functionality. If set to true, aeacus will not attempt to connect to the remote shell.
-
-```
-disableshell = false
-```
-
-**version**: Version of aeacus that the configuration was built for. Primarily used for compatibility checks, the engine will throw a warning if the binary version does not match the version specified in this field. In the future, this may also be used for backwards compatibility functionality.
-
-```
-version = "1.6.0"
-```
diff --git a/docs/regex.md b/docs/regex.md
new file mode 100644
index 00000000..0f713502
--- /dev/null
+++ b/docs/regex.md
@@ -0,0 +1,51 @@
+# Regular Expressions
+
+Many of the checks in `aeacus` require regular expression (regex) strings as input. This may seem inconvenient if you want to score something simple, but we think it significantly increases the overall quality of checks. Each regex is applied to each line of the input file, so currently, no multi-line regexes are currently possible.
+
+> We're using the Golang Regular Expression package ([documentation here](https://godocs.io/regexp)). It uses RE2 syntax, which is also generally the same as Perl, Python, and other languages.
+
+If you're unfamiliar, a 'regex' is just a way of describing a pattern of text. Let's say I was trying to score this in `/etc/apt/apt.conf.d/*`:
+
+```
+APT::Periodic::Update-Package-Lists "1";
+```
+
+The most simple regexes work the same way that normal CTRL-f searches work. It just matches what it is. A valid regex to score this would be `APT::Periodic::Update-Package-Lists "1";`, since none of those characters mean anything special to regex. With normal substring searching (like CTRL-f), that's the most specific we would be able to be.
+
+But, what about this?
+
+```
+APT::Periodic::Update-Package-Lists "1";
+```
+
+Notice that there are now two spaces before the `"1"`. That's a bummer, because our config is still valid to Ubuntu's software updater, but it's not scored as correct. We need at least one space between those two, so we'll do `APT::Periodic::Update-Package-Lists\s+"1";`, where `\s` means any whitespace, and `+` means 'at least one.'
+
+Similarly, what about all of these?
+
+```
+APT::Periodic::Update-Package-Lists "1";
+APT::Periodic::Update-Package-Lists "1" ;
+ APT::Periodic::Update-Package-Lists "1";
+```
+
+Our new regex, to match all of those, would be `\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*`. `*` means 'any amount of the preceding token, including none.' So this will match any amount of whitespace (including no whitespace). Which would work much better.
+
+But, what about this?
+
+```
+# APT::Periodic::Update-Package-Lists "1";
+```
+
+That ruins everything. It would score as correct even though it's commented out. But, it's an easy fix. With the regex `^\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*$`, where we added `^` for 'start of the line' and `$` for 'end of the line', it will only match if there's nothing except whitespace before and after the directive.
+
+But, what about this?
+
+```
+APT::PERIODIC::UPDATE-PACKAGE-LISTS "1";
+```
+
+Believe it or not, the apt configs appear to be case insensitive. So we modify the expression to be case insensitive with `(?i)`: `(?i)^\s*APT::Periodic::Update-Package-Lists\s+"1"\s*;\s*$`.
+
+As far as I know, this is as correct as we can get it.
+
+Thinking about the edge cases and correct grammar for scoring these directives is very important and makes a big difference in scoring robustness, which is why we use regexes for many checks. It can take a lot of practice to get a working expression, and making mistakes is very common. If you want to test your expression interactively, you can use something like [debuggex](debuggex.com).
diff --git a/docs/security.md b/docs/security.md
new file mode 100644
index 00000000..2afc826e
--- /dev/null
+++ b/docs/security.md
@@ -0,0 +1,14 @@
+# Security
+
+Engines that work like `aeacus` can never be "secure." We can only make it more difficult to crack.
+
+As long as the configuration is being loaded onto a virtual machine that is controlled by a competitor, there is no way to make it impossible to reveal what checks are being run. This is because they have control of the disk and CPU.
+
+Due to this fact, we focus on obfuscating and encrypting the configuration such that it would take at least a fair bit of reversing expertise and time in order to crack. The primary target audience for this style of competition will likely not be able or willing to do that.
+
+If you need a perfectly secure engine, there is no such thing. If you need an almost perfectly secure engine, you will need to write your own, and it will need to have two things:
+
+- Cloud-based VMs on controlled and monitored in-person thin-client kiosks with no external internet access.
+- Scoring engine that works at the VM infrastructure level, and reads in competitor VM disk files to score images.
+
+And even then, it's definitely breakable given some time. In any case, `aeacus` crypto with a few tweaks is probably suitable for your use case.
diff --git a/docs/securitypolicy.md b/docs/securitypolicy.md
index 7379e170..72723e44 100644
--- a/docs/securitypolicy.md
+++ b/docs/securitypolicy.md
@@ -1,12 +1,14 @@
# aeacus
-## Windows Security Settings 🤯🔫
+## Windows Security Settings
> A note on using `secedit.exe` and just parsing it... even [more reputable projects](https://github.com/dsccommunity/SecurityPolicyDsc/blob/8c318e43171cd32b14fe914b9c18c307093ba964/Modules/SecurityPolicyResourceHelper/SecurityPolicyResourceHelper.psm1) found it to be usable solution.
> List is sourced from `secedit.exe` and [this god-awful spreadsheet from Microsoft](https://www.microsoft.com/en-us/download/details.aspx?id=25250).
-### Account Policies
+These are all aliases. You can (and probably should) use the RegistryKey check for more control and to score obscure policies.
+
+## Account Policies
### Password Policies
@@ -61,6 +63,94 @@ Everything from this point and below should be in the `Security Options` pane of
- `NewAdministratorName`
- `NewGuestName`
-## Security Options
-I'm seriously going to suffer brain damage if I have to format all of these again... [See the spreadsheet](https://docs.google.com/spreadsheets/d/1N7uuke4Jg1R9FBhj8o5dxJQtEntQlea0McYz5upaiTk/edit#gid=1772229936).
+### Other Options
+
+Welcome to hell.
+
+| Aeacus Key Name | Policy Name | Registry Key |
+|------------------------------|--------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------|
+| LimitBlankPasswordUse | Accounts: Limit local account use of blank passwords to console logon only | `MACHINE\System\CurrentControlSet\Control\Lsa\LimitBlankPasswordUse` |
+| AuditBaseObjects | Audit: Audit the accesss of global system objects | `9:44:09 PM` |
+| FullPrivilegeAuditing | Audit: Audit the use of Backup and Restore privilege | `MACHINE\System\CurrentControlSet\Control\Lsa\FullPrivilegeAuditing` |
+| SCENoApplyLegacyAuditPolicy | Audit: Force audit policy subcategory settings (Windows Vista or later) to override audit policy category settings | `MACHINE\System\CurrentControlSet\Control\Lsa\SCENoApplyLegacyAuditPolicy` |
+| CrashOnAuditFail | Audit: Shut down system immediately if unable to log security audits | `MACHINE\System\CurrentControlSet\Control\Lsa\CrashOnAuditFail` |
+| MachineAccessRestriction | DCOM: Machine Access Restrictions in Security Descriptor Definition Language (SDDL) syntax | `MACHINE\SOFTWARE\policies\Microsoft\windows NT\DCOM\MachineAccessRestriction` |
+| MachineLaunchRestriction | DCOM: Machine Launch Restrictions in Security Descriptor Definition Language (SDDL) syntax | `MACHINE\SOFTWARE\policies\Microsoft\windows NT\DCOM\MachineLaunchRestriction` |
+| UndockWithoutLogon | Devices: Allow undock without having to log on | `MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\UndockWithoutLogon` |
+| AllocateDASD | Devices: Allowed to format and eject removable media | `MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\AllocateDASD` |
+| AddPrinterDrivers | Devices: Prevent users from installing printer drivers | `MACHINE\System\CurrentControlSet\Control\Print\Providers\LanMan Print Services\Servers\AddPrinterDrivers` |
+| AllocateCDRoms | Devices: Restrict CD-ROM access to locally logged-on user only | `MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\AllocateCDRoms` |
+| AllocateFloppies | Devices: Restrict floppy access to locally logged-on user only | `MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\AllocateFloppies` |
+| SubmitControl | Domain controller: Allow server operators to schedule tasks | `MACHINE\System\CurrentControlSet\Control\Lsa\SubmitControl` |
+| LDAPServerIntegrity | Domain controller: LDAP server signing requirements | `MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LDAPServerIntegrity` |
+| RefusePasswordChange | Domain controller: Refuse machine account password changes | `MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\RefusePasswordChange` |
+| RequireSignOrSeal | Domain member: Digitally encrypt or sign secure channel data (always) | `MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\RequireSignOrSeal` |
+| SealSecureChannel | Domain member: Digitally encrypt secure channel data (when possible) | `MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\SealSecureChannel` |
+| SignSecureChannel | Domain member: Digitally sign secure channel data (when possible) | `MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\SignSecureChannel` |
+| DisablePasswordChange | Domain member: Disable machine account password changes | `MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\DisablePasswordChange` |
+| MaximumPasswordAge | Domain member: Maximum machine account password age | `MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\MaximumPasswordAge` |
+| RequireStrongKey | Domain member: Require strong (Windows 2000 or later) session key | `MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\RequireStrongKey` |
+| DontDisplayLockedUserId | Interactive Logon: Display user information when session is locked | `MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System, value=DontDisplayLockedUserId` |
+| DisableCAD | Interactive logon: Do not require CTRL+ALT+DEL | `MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\DisableCAD` |
+| DontDisplayLastUserName | Interactive logon: Don't display last signed-in | `MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\DontDisplayLastUserName` |
+| LegalNoticeText | Interactive logon: Message text for users attempting to logon | `MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\LegalNoticeText` |
+| LegalNoticeCaption | Interactive logon: Message title for users attempting to logon | `MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\LegalNoticeCaption` |
+| CachedLogonsCount | Interactive logon: Number of previous logons to cache (in case domain controller is not available) | `MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\CachedLogonsCount` |
+| PasswordExpiryWarning | Interactive logon: Prompt user to change password before expiration | `MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\PasswordExpiryWarning` |
+| ForceUnlockLogon | Interactive logon: Require Domain Controller authentication to unlock workstation | `MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\ForceUnlockLogon` |
+| ScForceOption | Interactive logon: Require smart card | `MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\ScForceOption` |
+| ScRemoveOption | Interactive logon: Smart card removal behavior | `MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Winlogon\ScRemoveOption` |
+| EnableSecuritySignature | Microsoft network client: Digitally sign communications (if server agrees) | `MACHINE\System\CurrentControlSet\Services\LanmanWorkstation\Parameters\EnableSecuritySignature` |
+| EnablePlainTextPassword | Microsoft network client: Send unencrypted password to third-party SMB servers | `MACHINE\System\CurrentControlSet\Services\LanmanWorkstation\Parameters\EnablePlainTextPassword` |
+| AutoDisconnect | Microsoft network server: Amount of idle time required before suspending session | `MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\AutoDisconnect` |
+| RequireSecuritySignature | Microsoft network server: Digitally sign communications (always) | `MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\RequireSecuritySignature` |
+| EnableForcedLogOff | Microsoft network server: Disconnect clients when logon hours expire | `MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\EnableForcedLogOff` |
+| SmbServerNameHardeningLevel | Microsoft network server: Server SPN target name validation level | `MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\SmbServerNameHardeningLevel` |
+| RestrictAnonymousSAM | Network access: Do not allow anonymous enumeration of SAM accounts | `MACHINE\System\CurrentControlSet\Control\Lsa\RestrictAnonymousSAM` |
+| RestrictAnonymous | Network access: Do not allow anonymous enumeration of SAM accounts and shares | `MACHINE\System\CurrentControlSet\Control\Lsa\RestrictAnonymous` |
+| DisableDomainCreds | Network access: Do not allow storage of passwords and credentials for network authentication | `MACHINE\System\CurrentControlSet\Control\Lsa\DisableDomainCreds` |
+| EveryoneIncludesAnonymous | Network access: Let Everyone permissions apply to anonymous users | `MACHINE\System\CurrentControlSet\Control\Lsa\EveryoneIncludesAnonymous` |
+| NullSessionPipes | Network access: Named Pipes that can be accessed anonymously | `MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\NullSessionPipes` |
+| Machine | Network access: Remotely accessible registry paths | `MACHINE\System\CurrentControlSet\Control\SecurePipeServers\Winreg\AllowedPaths\Machine` |
+| N/A | Network access: Remotely accessible registry paths and sub-paths | `MACHINE\System\CurrentControlSet\Control\SecurePipeServers\Winreg\AllowedPaths\Machine` |
+| NullSessionShares | Network access: Restrict anonymous access to Named Pipes and Shares | `MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\NullSessionShares` |
+| NullSessionShares | Network access: Shares that can be accessed anonymously | `MACHINE\System\CurrentControlSet\Services\LanManServer\Parameters\NullSessionShares` |
+| ForceGuest | Network access: Sharing and security model for local accounts | `MACHINE\System\CurrentControlSet\Control\Lsa\ForceGuest` |
+| UseMachineId | Network security: Allow Local System to use computer identity for NTLM | `MACHINE\System\CurrentControlSet\Control\Lsa\UseMachineId` |
+| allownullsessionfallback | Network security: Allow LocalSystem NULL session fallback | `MACHINE\System\CurrentControlSet\Control\Lsa\MSV1_0\allownullsessionfallback` |
+| AllowOnlineID | Network security: Allow PKU2U authentication requests to this computer to use online identities. | `MACHINE\System\CurrentControlSet\Control\Lsa\pku2u\AllowOnlineID` |
+| SupportedEncryptionTypes | Network security: Configure encryption types allowed for Kerberos | `MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\Kerberos\Parameters\SupportedEncryptionTypes` |
+| NoLMHash | Network security: Do not store LAN Manager hash value on next password change | `MACHINE\System\CurrentControlSet\Control\Lsa\NoLMHash` |
+| LmCompatibilityLevel | Network security: LAN Manager authentication level | `MACHINE\System\CurrentControlSet\Control\Lsa\LmCompatibilityLevel` |
+| LDAPClientIntegrity | Network security: LDAP client signing requirements | `MACHINE\System\CurrentControlSet\Services\LDAP\LDAPClientIntegrity` |
+| NTLMMinClientSec | Network security: Minimum session security for NTLM SSP based (including secure RPC) clients | `MACHINE\System\CurrentControlSet\Control\Lsa\MSV1_0\NTLMMinClientSec` |
+| NTLMMinServerSec | Network security: Minimum session security for NTLM SSP based (including secure RPC) servers | `MACHINE\System\CurrentControlSet\Control\Lsa\MSV1_0\NTLMMinServerSec` |
+| RestrictNTLMInDomain | Network security: Restrict NTLM: NTLM authentication in this domain | `MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\RestrictNTLMInDomain` |
+| ClientAllowedNTLMServers | Network security: Restrict NTLM: Add remote server exceptions for NTLM authentication | `MACHINE\System\CurrentControlSet\Control\Lsa\MSV1_0\ClientAllowedNTLMServers` |
+| DCAllowedNTLMServers | Network security: Restrict NTLM: Add server exceptions in this domain | `MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\DCAllowedNTLMServers` |
+| AuditReceivingNTLMTraffic | Network security: Restrict NTLM: Audit Incoming NTLM Traffic | `MACHINE\System\CurrentControlSet\Control\Lsa\MSV1_0\AuditReceivingNTLMTraffic` |
+| AuditNTLMInDomain | Network security: Restrict NTLM: Audit NTLM authentication in this domain | `MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\AuditNTLMInDomain` |
+| RestrictReceivingNTLMTraffic | Network security: Restrict NTLM: Incoming NTLM traffic | `MACHINE\System\CurrentControlSet\Control\Lsa\MSV1_0\RestrictReceivingNTLMTraffic` |
+| RestrictSendingNTLMTraffic | Network security: Restrict NTLM: Outgoing NTLM traffic to remote servers | `MACHINE\System\CurrentControlSet\Control\Lsa\MSV1_0\RestrictSendingNTLMTraffic` |
+| SecurityLevel | Recovery console: Allow automatic administrative logon | `MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Setup\RecoveryConsole\SecurityLevel` |
+| SetCommand | Recovery console: Allow floppy copy and access to all drives and all folders | `MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Setup\RecoveryConsole\SetCommand` |
+| ShutdownWithoutLogon | Shutdown: Allow system to be shut down without having to log on | `MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\ShutdownWithoutLogon` |
+| ClearPageFileAtShutdown | Shutdown: Clear virtual memory pagefile | `MACHINE\System\CurrentControlSet\Control\Session Manager\Memory Management\ClearPageFileAtShutdown` |
+| ForceKeyProtection | System cryptography: Force strong key protection for user keys stored on the computer | `MACHINE\Software\Policies\Microsoft\Cryptography\ForceKeyProtection` |
+| FIPSAlgorithmPolicy | System cryptography: Use FIPS compliant algorithms for encryption, hashing, and signing | `MACHINE\System\CurrentControlSet\Control\Lsa\FIPSAlgorithmPolicy` |
+| NoDefaultAdminOwner | System objects: Default owner for objects created by members of the Administrators group | `MACHINE\System\CurrentControlSet\Control\Lsa\NoDefaultAdminOwner` |
+| ObCaseInsensitive | System objects: Require case insensitivity for non-Windows subsystems | `MACHINE\System\CurrentControlSet\Control\Session Manager\Kernel\ObCaseInsensitive` |
+| ProtectionMode | System objects: Strengthen default permissions of internal system objects (e.g., Symbolic Links) | `MACHINE\System\CurrentControlSet\Control\Session Manager\ProtectionMode` |
+| optional | System settings: Optional subsystems | `MACHINE\System\CurrentControlSet\Control\Session Manager\SubSystems\optional` |
+| AuthenticodeEnabled | System settings: Use Certificate Rules on Windows Executables for Software Restriction Policies | `MACHINE\Software\Policies\Microsoft\Windows\Safer\CodeIdentifiers\AuthenticodeEnabled` |
+| FilterAdministratorToken | User Account Control: Admin Approval Mode for the Built-in Administrator account | `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\FilterAdministratorToken` |
+| EnableUIADesktopToggle | User Account Control: Allow UIAccess applications to prompt for elevation without using the secure desktop. | `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\EnableUIADesktopToggle` |
+| ConsentPromptBehaviorAdmin | User Account Control: Behavior of the elevation prompt for administrators in Admin Approval Mode | `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\ConsentPromptBehaviorAdmin` |
+| ConsentPromptBehaviorUser | User Account Control: Behavior of the elevation prompt for standard users | `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\ConsentPromptBehaviorUser` |
+| EnableInstallerDetection | User Account Control: Detect application installations and prompt for elevation | `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\EnableInstallerDetection` |
+| ValidateAdminCodeSignatures | User Account Control: Only elevate executables that are signed and validated | `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\ValidateAdminCodeSignatures` |
+| EnableSecureUIAPaths | User Account Control: Only elevate UIAccess applications that are installed in secure locations | `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\EnableSecureUIAPaths` |
+| EnableLUA | User Account Control: Run all administrators in Admin Approval Mode | `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\EnableLUA` |
+| PromptOnSecureDesktop | User Account Control: Switch to the secure desktop when prompting for elevation | `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\PromptOnSecureDesktop` |
+| EnableVirtualization | User Account Control: Virtualize file and registry write failures to per-user locations | `SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\EnableVirtualization` |
diff --git a/docs/v2.md b/docs/v2.md
new file mode 100644
index 00000000..9fe0889e
--- /dev/null
+++ b/docs/v2.md
@@ -0,0 +1,41 @@
+# Breaking Changes
+
+- Checks now use semantic field names in `scoring.conf`. For example, the following `FileContains` check:
+
+```
+[[check]]
+message = "Removed insecure sudoers rule"
+points = 10
+ [[check.pass]]
+ type="FileContainsNot"
+ arg1="/etc/sudoers"
+ arg2="NOPASSWD"
+```
+
+Can now be written as:
+
+```
+[[check]]
+message = "Removed insecure sudoers rule"
+points = 10
+ [[check.pass]]
+ type = "FileContainsNot"
+ path = "/etc/sudoers"
+ value = "NOPASSWD"
+```
+
+Please see [checks.md](./checks.md) for a detailed list of all parameters.
+
+- `FileContains` and `DirContains` use regex by default. `FileContainsRegex` and `DirContainsRegex` call these functions for backwards compatibility reasons as of v2.0.0, but these aliases may be phased out in the future
+
+# Changes for Developers
+
+- In order to call scoring functions, you must construct _or_ use an existing `check` and call the appropriate method like so:
+
+```
+result, err := cond{
+ SomeKey: "value"
+}.Method()
+```
+
+- The `cmd` structure no longer exists, so you don't need to call functions that resided under `cmd/` using the `cmd.` prefix when referring to them in `aeacus.go` and `phocus.go`
diff --git a/go.mod b/go.mod
index 51125a63..3e3983b7 100644
--- a/go.mod
+++ b/go.mod
@@ -1,20 +1,35 @@
module github.com/elysium-suite/aeacus
-go 1.15
+go 1.18
require (
- github.com/BurntSushi/toml v0.3.1
- github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
- github.com/fatih/color v1.10.0
+ github.com/ActiveState/termtest/conpty v0.5.0
+ github.com/BurntSushi/toml v1.0.0
+ github.com/creack/pty v1.1.17
+ github.com/fatih/color v1.13.0
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1
- github.com/go-ole/go-ole v1.2.5 // indirect
- github.com/godbus/dbus/v5 v5.0.4 // indirect
- github.com/gopherjs/gopherjs v0.0.0-20210519211817-2312de329ae4 // indirect
- github.com/gorilla/websocket v1.4.2
- github.com/iamacarpet/go-win64api v0.0.0-20210311141720-fe38760bed28
+ github.com/gorilla/websocket v1.5.0
+ github.com/iamacarpet/go-win64api v0.0.0-20220314100901-d3a958911279
github.com/judwhite/go-svc v1.2.1
+ github.com/urfave/cli/v2 v2.4.0
+ golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86
+ golang.org/x/text v0.3.7
+)
+
+require (
+ github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
+ github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 // indirect
+ github.com/godbus/dbus/v5 v5.0.3 // indirect
+ github.com/google/cabbie v1.0.2 // indirect
+ github.com/google/glazier v0.0.0-20211029225403-9f766cca891d // indirect
+ github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c // indirect
+ github.com/gopherjs/gopherwasm v1.1.0 // indirect
+ github.com/mattn/go-colorable v0.1.9 // indirect
+ github.com/mattn/go-isatty v0.0.14 // indirect
+ github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
- github.com/urfave/cli/v2 v2.3.0
- golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea
- golang.org/x/text v0.3.5
+ github.com/scjalliance/comshim v0.0.0-20190308082608-cf06d2532c4e // indirect
+ github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af // indirect
)
diff --git a/go.sum b/go.sum
index e31d6a80..dd9dfd3c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,364 +1,143 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
-cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
-cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
-cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
-cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
-cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
-cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
-cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
-dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+bitbucket.org/creachadair/stringset v0.0.9/go.mod h1:t+4WcQ4+PXTa8aQdNKe40ZP6iwesoMFWAxPGd3UGjyY=
+github.com/ActiveState/termtest/conpty v0.5.0 h1:JLUe6YDs4Jw4xNPCU+8VwTpniYOGeKzQg4SM2YHQNA8=
+github.com/ActiveState/termtest/conpty v0.5.0/go.mod h1:LO4208FLsxw6DcNZ1UtuGUMW+ga9PFtX4ntv8Ymg9og=
+github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
+github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
-github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
-github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
-github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
-github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
-github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
-github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
-github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
-github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
-github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
-github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
-github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
+github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
+github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/StackExchange/wmi v1.2.0/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
+github.com/capnspacehook/taskmaster v0.0.0-20210519235353-1629df7c85e9/go.mod h1:257CYs3Wd/CTlLQ3c72jKv+fFE2MV3WPNnV5jiroYUU=
+github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
+github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/creachadair/staticfile v0.1.3/go.mod h1:a3qySzCIXEprDGxk6tSxSI+dBBdLzqeBOMhZ+o2d3pM=
+github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
+github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
+github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
+github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28 h1:M2Zt3G2w6Q57GZndOYk42p7RvMeO8izO8yKTfIxGqxA=
-github.com/gen2brain/beeep v0.0.0-20200526185328-e9c15c258e28/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8=
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1 h1:Xh9mvwEmhbdXlRSsgn+N0zj/NqnKvpeqL08oKDHln2s=
github.com/gen2brain/beeep v0.0.0-20210529141713-5586760f0cc1/go.mod h1:ElSskYZe3oM8kThaHGJ+kiN2yyUMVXMZ7WxF9QqLDS8=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
-github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
-github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
-github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
-github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY=
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
-github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4 h1:qZNfIGkIANxGv/OqtnntR4DfOY2+BgwR60cAcu/i3SE=
github.com/go-toast/toast v0.0.0-20190211030409-01e6764cf0a4/go.mod h1:kW3HQ4UdaAyrUCSSDR4xUzBKW6O2iA4uHhk7AtyYp10=
+github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/aukera v0.0.0-20201117230544-d145c8357fea/go.mod h1:oXqTZORBzdwQ6L32YjJmaPajqIV/hoGEouwpFMf4cJE=
+github.com/google/cabbie v1.0.2 h1:UtB+Nn6fPB43wGg5xs4tgU+P3hTZ6KsulgtaHtqZZfs=
+github.com/google/cabbie v1.0.2/go.mod h1:6MmHaUrgfabehCHAIaxdrbmvHSxUVXj3Abs08FMABSo=
+github.com/google/glazier v0.0.0-20210617205946-bf91b619f5d4/go.mod h1:g7oyIhindbeebnBh0hbFua5rv6XUt/nweDwIWdvxirg=
+github.com/google/glazier v0.0.0-20211029225403-9f766cca891d h1:GBIF4RkD4E9USvSRT4O4tBCT77JExIr+qnruI9nkJQo=
+github.com/google/glazier v0.0.0-20211029225403-9f766cca891d/go.mod h1:h2R3DLUecGbLSyi6CcxBs5bdgtJhgK+lIffglvAcGKg=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
-github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
-github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
-github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
-github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/logger v1.1.0/go.mod h1:w7O8nrRr0xufejBlQMI83MXqRusvREoJdaAxV+CoAB4=
+github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ=
+github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/winops v0.0.0-20210803215038-c8511b84de2b/go.mod h1:ShbX8v8clPm/3chw9zHVwtW3QhrFpL8mXOwNxClt4pg=
+github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c h1:16eHWuMGvCjSfgRJKqIzapE78onvvTbdi1rMkU00lZw=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe h1:rcf1P0fm+1l0EjG16p06mYLj9gW9X36KgdHJ/88hS4g=
-github.com/gopherjs/gopherjs v0.0.0-20210202160940-bed99a852dfe/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
-github.com/gopherjs/gopherjs v0.0.0-20210519211817-2312de329ae4 h1:zG8Qj6tk6cilKBocXnebVacg319fNEKzY0jVvtTw7wQ=
-github.com/gopherjs/gopherjs v0.0.0-20210519211817-2312de329ae4/go.mod h1:Opf9rtYVq0eTyX+aRVmRO9hE8ERAozcdrBxWG9Q6mkQ=
github.com/gopherjs/gopherwasm v1.1.0 h1:fA2uLoctU5+T3OhOn2vYP0DVT6pxc7xhTlBB1paATqQ=
github.com/gopherjs/gopherwasm v1.1.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI=
-github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
-github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
-github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
-github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
-github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
-github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
-github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
-github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
-github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
-github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
-github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
-github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
-github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
-github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
-github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
-github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
-github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
-github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
-github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
-github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
-github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
-github.com/iamacarpet/go-win64api v0.0.0-20210311141720-fe38760bed28 h1:QhDPvIcXXFltItF7kQ2Go4frViywCx9xDl2okzLNt+A=
+github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
+github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/groob/plist v0.0.0-20210519001750-9f754062e6d6/go.mod h1:itkABA+w2cw7x5nYUS/pLRef6ludkZKOigbROmCTaFw=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/iamacarpet/go-win64api v0.0.0-20210311141720-fe38760bed28/go.mod h1:oGJx9dz0Ny7HC7U55RZ0Smd6N9p3hXP/+hOFtuYrAxM=
-github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
-github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
-github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
-github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
+github.com/iamacarpet/go-win64api v0.0.0-20220314100901-d3a958911279 h1:T1atnl3wZ5m6SaVM7qimhKUKwRcjdloacML8KPftQxA=
+github.com/iamacarpet/go-win64api v0.0.0-20220314100901-d3a958911279/go.mod h1:B7zFQPAznj+ujXel5X+LUoK3LgY6VboCdVYHZNn7gpg=
github.com/judwhite/go-svc v1.2.1 h1:a7fsJzYUa33sfDJRF2N/WXhA+LonCEEY8BJb1tuS5tA=
github.com/judwhite/go-svc v1.2.1/go.mod h1:mo/P2JNX8C07ywpP9YtO2gnBgnUiFTHqtsZekJrUuTk=
-github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
-github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
-github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
-github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
-github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
-github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
-github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
-github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
+github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
+github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
-github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
-github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
-github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
-github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
-github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
-github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
-github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
-github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
-github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
+github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
+github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
-github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
-github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
-github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
-github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
-github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
-github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
-github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
-github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
-github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/rickb777/date v1.14.2/go.mod h1:swmf05C+hN+m8/Xh7gEq3uB6QJDNc5pQBWojKdHetOs=
+github.com/rickb777/plural v1.2.2/go.mod h1:xyHbelv4YvJE51gjMnHvk+U2e9zIysg6lTnSQK8XUYA=
+github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
-github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
-github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
-github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
-github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
-github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
-github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
-github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
-github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo=
-github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
-github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/scjalliance/comshim v0.0.0-20190308082608-cf06d2532c4e h1:+/AzLkOdIXEPrAQtwAeWOBnPQ0BnYlBW0aCZmSb47u4=
+github.com/scjalliance/comshim v0.0.0-20190308082608-cf06d2532c4e/go.mod h1:9Tc1SKnfACJb9N7cw2eyuI6xzy845G7uZONBsi5uPEA=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af h1:6yITBqGTE2lEeTPG04SN9W+iWHCRyHqlVYILiSXziwk=
github.com/tadvi/systray v0.0.0-20190226123456-11a2b8fa57af/go.mod h1:4F09kP5F+am0jAwlQLddpoMDM+iewkxxt6nxUQ5nq5o=
-github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
-github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
-github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
-github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
-go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
-go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
-go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
-go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
-go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
-golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
-golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I=
+github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
-golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
-golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
-golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
-golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
-golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
-golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200428200454-593003d681fa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200622182413-4b0db7f3f76b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210324051608-47abb6519492 h1:Paq34FxTluEPvVyayQqMPgHm+vTOrIifmcYxFBx9TLg=
-golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea h1:+WiDlPBBaO+h9vPNZi8uJ3k4BkKQB7Iow3aqwHVA5hI=
-golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210601080250-7ecdf8ef093b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211107104306-e0b2ad06fe42/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc=
+golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
-google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
-google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
-google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
-google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
-google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
-google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
-gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
-gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
-rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/gui_linux.go b/gui_linux.go
new file mode 100644
index 00000000..27b70b6e
--- /dev/null
+++ b/gui_linux.go
@@ -0,0 +1,8 @@
+package main
+
+func launchIDPrompt() {
+ err := shellCommand(`zenity --title "Team ID Prompt" --text "Enter your Team ID below!" --entry > ` + dirPath + "TeamID.txt")
+ if err != nil {
+ fail("Error running ID prompt command: " + err.Error())
+ }
+}
diff --git a/cmd/gui_windows.go b/gui_windows.go
similarity index 91%
rename from cmd/gui_windows.go
rename to gui_windows.go
index 463a65df..3b206a01 100644
--- a/cmd/gui_windows.go
+++ b/gui_windows.go
@@ -1,7 +1,7 @@
-package cmd
+package main
-// LaunchIDPrompt launches an ID Prompt on Windows using PowerShell
-func LaunchIDPrompt() {
+// launchIDPrompt launches an ID Prompt on Windows using PowerShell
+func launchIDPrompt() {
powerShellPrompt := `
Start-Service -Name CSSClient -ErrorAction SilentlyContinue
$teamIDContent = Get-Content C:\aeacus\TeamID.txt
@@ -57,8 +57,3 @@ func LaunchIDPrompt() {
`
shellCommand(powerShellPrompt)
}
-
-// LaunchConfigGui (WIP) launches a configuration GUI on Windows
-func LaunchConfigGui() {
- warnPrint("This feature is not supported yet on Windows.")
-}
diff --git a/cmd/info_windows.go b/info_windows.go
similarity index 55%
rename from cmd/info_windows.go
rename to info_windows.go
index 9bbd605d..8920b686 100644
--- a/cmd/info_windows.go
+++ b/info_windows.go
@@ -1,34 +1,34 @@
-package cmd
+package main
import (
"fmt"
"os"
)
-// GetInfo is a helper function to retrieve
-// generic information about the system
-func GetInfo(infoType string) {
+// getInfo is a helper function to retrieve information about the
+// system.
+func getInfo(infoType string) {
switch infoType {
case "programs":
programList, _ := getPrograms()
for _, p := range programList {
- infoPrint(p)
+ info(p)
}
case "users":
userList, _ := getLocalUsers()
for _, u := range userList {
- infoPrint(fmt.Sprint(u))
+ info(fmt.Sprint(u))
}
case "admins":
adminList, _ := getLocalAdmins()
for _, u := range adminList {
- infoPrint(fmt.Sprint(u))
+ info(fmt.Sprint(u))
}
default:
if infoType == "" {
- failPrint("No info type provided.")
+ fail("No info type provided. See the README for supported types.")
} else {
- failPrint("No info for \"" + infoType + "\" found.")
+ fail("No info for \"" + infoType + "\" found.")
}
os.Exit(1)
}
diff --git a/misc/desktop/StopScoring.desktop b/misc/desktop/StopScoring.desktop
index 8b8f358e..a54f400d 100644
--- a/misc/desktop/StopScoring.desktop
+++ b/misc/desktop/StopScoring.desktop
@@ -3,6 +3,6 @@ Encoding=UTF-8
Version=1.0
Type=Application
Terminal=true
-Exec=/usr/bin/sudo /bin/bash /opt/aeacus/assets/stop_scoring.sh
+Exec=/usr/bin/sudo /bin/bash /opt/aeacus/assets/scripts/stop_scoring.sh
Name=Stop Scoring
Icon=/opt/aeacus/assets/img/logo.png
diff --git a/misc/dev/gen-crypto.sh b/misc/dev/gen-crypto.sh
index fb049509..ab742b39 100644
--- a/misc/dev/gen-crypto.sh
+++ b/misc/dev/gen-crypto.sh
@@ -1,28 +1,21 @@
#!/bin/sh
+set -e
rand() { xxd -l 64 -c 64 -p /dev/urandom; }
-replace() { sed -i "s/$1/$2/g" cmd/crypto.go.tmpl; }
+replace() { sed -i "s/$1/$2/g" crypto.go; }
hashOne=$(rand)
hashTwo=$(rand)
byteKey=$(rand | sed 's/\(..\)/0x\1, /g')
-cp cmd/crypto.go.tmpl cmd/crypto.go.tmpl.bak
+if [ -f "crypto.go.bak" ]; then
+ mv crypto.go.bak crypto.go
+fi
-replace "HASH_ONE" "$hashOne"
-replace "HASH_TWO" "$hashTwo"
-replace "BYTE_KEY" "$byteKey"
-
-cp cmd/crypto.go.tmpl cmd/crypto.go
-cp cmd/crypto.go.tmpl.bak cmd/crypto.go.tmpl
-rm cmd/crypto.go.tmpl.bak
+cp crypto.go crypto.go.bak
-echo "generated crypto.go"
-
-cat <<-EOF >misc/.keys
- hash one: "$hashOne"
- hash two: "$hashTwo"
- byte key: []byte{$byteKey}
-EOF
+replace "HASH_ONE" "$hashOne"
+replace "SECOND_HASH" "$hashTwo"
+replace "0x01" "`echo $byteKey | head -c 382`"
-echo "generated .keys"
+echo "Generated random keys for crypto.go"
diff --git a/misc/dev/install.sh b/misc/dev/install.sh
deleted file mode 100755
index f085ba2a..00000000
--- a/misc/dev/install.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/sh
-
-cat <<-EOF
-
- .oooo. .ooooo. .oooo. .ooooo. oooo oooo .oooo.o
-\`P )88b d88' \`88b \`P )88b d88' \`"Y8 \`888 \`888 d88( "8
- .oP"888 888ooo888 .oP"888 888 888 888 \`"Y88b.
-d8( 888 888 .o d8( 888 888 .o8 888 888 o. )88b
-\`Y888""8o \`Y8bod8P' \`Y888""8o \`Y8bod8P' \`V88V"V8P' 8""888P'
-
-This script sets up the development environment on a Linux (Debian-based) box.
-
-EOF
-
-printf "\033[32;1m[+] Updating package lists\033[0m\n"
-sudo apt-get update
-
-printf "\n\033[32;1m[+] Installing Go\033[0m\n"
-wget -O ~/go.tar.gz https://golang.org/dl/go1.16.2.linux-amd64.tar.gz
-sudo tar -C /usr/local -xzf ~/go.tar.gz
-
-printf "\n\033[32;1m[+] Adding \`go\` binary to PATH\033[0m\n"
-echo "export PATH=$PATH:/usr/local/go/bin:/$HOME/go" | sudo tee -a /etc/profile
-
-printf "\n\033[32;1m[+] Installing Git & Make\033[0m\n"
-sudo apt-get install -y git make
-
-printf "\n\033[32;1m[+] Build dependencies installed successfully\033[0m\n"
-echo "Run \`source /etc/profile\` to add \`go\` and to your PATH"
-echo "Run go get -v -t -d ./... to install aeacus' dependencies"
-echo "Check out the \`Makefile\` to see what targets you can build Aeacus for"
diff --git a/misc/tests/dir/hello/3.txt b/misc/tests/dir/hello/3.txt
index fcf1663d..17a4aca5 100644
--- a/misc/tests/dir/hello/3.txt
+++ b/misc/tests/dir/hello/3.txt
@@ -27,6 +27,7 @@ abcdabcd
abcdabcd
abcdabcd
abcdabcd
+aaaa
abcdabcd
abcdabcd
abcdabcd
@@ -54,6 +55,7 @@ abcdabcd
abcdabcd
abcdabcd
abcdabcd
+spaces in it 15879163 nums
abcdabcd
abcdabcd
abcdabcd
diff --git a/output.go b/output.go
new file mode 100644
index 00000000..723d9a6a
--- /dev/null
+++ b/output.go
@@ -0,0 +1,98 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/fatih/color"
+)
+
+// confirm will prompt the user with the given toPrint string, and
+// exit the program if N or n is input.
+func confirm(p ...interface{}) {
+ if yesEnabled {
+ return
+ }
+ toPrint := fmt.Sprint(p...)
+ toPrint = printer(color.FgYellow, "CONF", toPrint)
+ fmt.Print(toPrint + " [Y/n]: ")
+ var resp string
+ fmt.Scanln(&resp)
+ if strings.ToLower(strings.TrimSpace(resp)) == "n" {
+ os.Exit(1)
+ }
+}
+
+// ask will prompt the user with the given toPrint string, and
+// return a boolean.
+func ask(p ...interface{}) bool {
+ if yesEnabled {
+ return true
+ }
+ toPrint := fmt.Sprint(p...)
+ toPrint = printer(color.FgBlue, "CONF", toPrint)
+ fmt.Print(toPrint + " [Y/n]: ")
+ var resp string
+ fmt.Scanln(&resp)
+ if strings.ToLower(strings.TrimSpace(resp)) == "n" {
+ return false
+ }
+ return true
+}
+
+func pass(p ...interface{}) {
+ toPrint := fmt.Sprintln(p...)
+ printStr := printer(color.FgGreen, "PASS", toPrint)
+ fmt.Printf(printStr)
+}
+
+func fail(p ...interface{}) {
+ toPrint := fmt.Sprintln(p...)
+ fmt.Printf(printer(color.FgRed, "FAIL", toPrint))
+}
+
+func warn(p ...interface{}) {
+ toPrint := fmt.Sprintln(p...)
+ fmt.Printf(printer(color.FgYellow, "WARN", toPrint))
+}
+
+func debug(p ...interface{}) {
+ if debugEnabled {
+ toPrint := fmt.Sprintln(p...)
+ printStr := printer(color.FgMagenta, "DBUG", toPrint)
+ fmt.Printf(printStr)
+ }
+}
+
+func info(p ...interface{}) {
+ if verboseEnabled {
+ toPrint := fmt.Sprintln(p...)
+ printStr := printer(color.FgCyan, "INFO", toPrint)
+ fmt.Printf(printStr)
+ }
+}
+
+func blue(head string, p ...interface{}) {
+ toPrint := fmt.Sprintln(p...)
+ printStr := printer(color.FgCyan, head, toPrint)
+ fmt.Printf(printStr)
+}
+
+func red(head string, p ...interface{}) {
+ toPrint := fmt.Sprintln(p...)
+ fmt.Printf(printer(color.FgRed, head, toPrint))
+}
+
+func green(head string, p ...interface{}) {
+ toPrint := fmt.Sprintln(p...)
+ fmt.Printf(printer(color.FgGreen, head, toPrint))
+}
+
+func printer(colorChosen color.Attribute, messageType, toPrint string) string {
+ printer := color.New(colorChosen, color.Bold)
+ printStr := "["
+ printStr += printer.Sprintf(messageType)
+ printStr += fmt.Sprintf("] %s", toPrint)
+ return printStr
+}
diff --git a/cmd/phocus.go b/phocus.go
similarity index 64%
rename from cmd/phocus.go
rename to phocus.go
index 9374f88e..37cec843 100644
--- a/cmd/phocus.go
+++ b/phocus.go
@@ -1,22 +1,23 @@
-package cmd
+package main
import (
"math/rand"
+ "os"
"time"
"github.com/urfave/cli/v2"
)
func phocusLoop() {
- infoPrint("Initializing engine context...")
+ info("Initializing engine context...")
phocusEnvironment()
+ if conf.Shell {
+ go shellSocket()
+ }
for {
- checkTrace()
- timeCheck()
- infoPrint("Scoring image...")
scoreImage()
jitter := time.Duration(rand.Intn(8) + 10)
- infoPrint("Scored image, sleeping for a bit...")
+ info("Scored image, sleeping for a bit...")
time.Sleep(jitter * time.Second)
}
}
@@ -25,22 +26,20 @@ func phocusLoop() {
// run on first start.
func phocusEnvironment() {
// Make sure we're running as admin.
- RunningPermsCheck()
- // Fill constants (ex. mc.DirPath) based on OS.
- FillConstants()
+ permsCheck()
// Make sure phocus is not being traced or debugged.
checkTrace()
// Read in scoring data from the scoring data file.
- if err := ReadScoringData(); err != nil {
- // uhh
+ if err := readScoringData(); err != nil {
+ fail(err)
+ os.Exit(1)
}
- // Seed the random function for scoring at random intervals.
+ // Seed the random function for scoring at "random" intervals.
rand.Seed(time.Now().UnixNano())
}
-// GenPhocusApp generates a basic CLI interface that is
-// OS-independent
-func GenPhocusApp() *cli.App {
+// genPhocusApp generates a basic CLI interface that is OS-independent.
+func genPhocusApp() *cli.App {
return &cli.App{
Name: "phocus",
Usage: "score vulnerabilities",
@@ -48,13 +47,20 @@ func GenPhocusApp() *cli.App {
phocusLoop()
return nil
},
+ Before: func(c *cli.Context) error {
+ err := determineDirectory()
+ if err != nil {
+ return err
+ }
+ return nil
+ },
Commands: []*cli.Command{
{
Name: "idprompt",
Aliases: []string{"p"},
Usage: "Launch TeamID GUI prompt",
Action: func(c *cli.Context) error {
- LaunchIDPrompt()
+ launchIDPrompt()
return nil
},
},
@@ -63,8 +69,7 @@ func GenPhocusApp() *cli.App {
Aliases: []string{"v"},
Usage: "Print the current version of phocus",
Action: func(c *cli.Context) error {
- infoPrint("=== phocus ===")
- infoPrint("version " + AeacusVersion)
+ info("phocus version " + version)
return nil
},
},
diff --git a/phocus_linux.go b/phocus_linux.go
index dae8e433..b7e03783 100644
--- a/phocus_linux.go
+++ b/phocus_linux.go
@@ -1,16 +1,14 @@
-// +build phocus
+//go:build phocus
package main
import (
"log"
"os"
-
- "github.com/elysium-suite/aeacus/cmd"
)
func main() {
- app := cmd.GenPhocusApp()
+ app := genPhocusApp()
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
diff --git a/phocus_windows.go b/phocus_windows.go
index 793fec9c..64bd7359 100644
--- a/phocus_windows.go
+++ b/phocus_windows.go
@@ -1,4 +1,4 @@
-// +build phocus
+//go:build phocus
package main
@@ -10,12 +10,11 @@ import (
"sync"
"time"
- "github.com/elysium-suite/aeacus/cmd"
"github.com/judwhite/go-svc"
)
func phocusStart(quit chan struct{}) {
- app := cmd.GenPhocusApp()
+ app := genPhocusApp()
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
@@ -32,7 +31,6 @@ type program struct {
quit chan struct{}
}
-// main for phocus_windows.go will
func main() {
flag.Parse()
prg := &program{}
@@ -77,6 +75,6 @@ func (p *program) Stop() error {
}
func launchIDPromptWrapper(quit chan struct{}) {
- cmd.LaunchIDPrompt()
+ launchIDPrompt()
os.Exit(0) // This is temporary solution
}
diff --git a/cmd/policies_windows.go b/policies_windows.go
similarity index 98%
rename from cmd/policies_windows.go
rename to policies_windows.go
index 21acabe1..9fa0b6f7 100644
--- a/cmd/policies_windows.go
+++ b/policies_windows.go
@@ -1,7 +1,7 @@
-package cmd
+package main
-// secpolToKey contains a large mapping onf securityPolicy
-// names or keys to registry locations.
+// secpolToKey contains a large mapping of securityPolicy names or keys to
+// registry locations.
var secpolToKey = map[string]string{
"LimitBlankPasswordUse": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\LimitBlankPasswordUse",
"AuditBaseObjects": "MACHINE\\System\\CurrentControlSet\\Control\\Lsa\\AuditBaseObjects",
diff --git a/release_linux.go b/release_linux.go
new file mode 100644
index 00000000..789084db
--- /dev/null
+++ b/release_linux.go
@@ -0,0 +1,118 @@
+package main
+
+// writeDesktopFiles creates TeamID.txt and its shortcut, as well as links
+// to the ScoringReport, ReadMe, and other needed files.
+func writeDesktopFiles() {
+ info("Creating or emptying TeamID.txt...")
+ shellCommand("echo 'YOUR-TEAMID-HERE' > " + dirPath + "TeamID.txt")
+ shellCommand("chmod 666 " + dirPath + "TeamID.txt")
+ shellCommand("chown " + conf.User + ":" + conf.User + " " + dirPath + "TeamID.txt")
+ info("Writing shortcuts to Desktop...")
+ shellCommand("mkdir -p /home/" + conf.User + "/Desktop/")
+ shellCommand("cp " + dirPath + "misc/desktop/*.desktop /home/" + conf.User + "/Desktop/")
+ shellCommand("chmod +x /home/" + conf.User + "/Desktop/*.desktop")
+ shellCommand("chown " + conf.User + ":" + conf.User + " /home/" + conf.User + "/Desktop/*")
+}
+
+// configureAutologin configures the auto-login capability for LightDM and
+// GDM3, so that the image automatically logs in to the main user's account
+// on boot.
+func configureAutologin() {
+ lightdm, _ := cond{Path: "/usr/share/lightdm"}.PathExists()
+ gdm, _ := cond{Path: "/etc/gdm3/"}.PathExists()
+ if lightdm {
+ info("LightDM detected for autologin.")
+ shellCommand(`echo "autologin-user=` + conf.User + `" >> /usr/share/lightdm/lightdm.conf.d/50-ubuntu.conf`)
+ } else if gdm {
+ info("GDM3 detected for autologin.")
+ shellCommand(`echo -e "AutomaticLoginEnable=True\nAutomaticLogin=` + conf.User + `" >> /etc/gdm3/daemon.conf`)
+ } else {
+ fail("Unable to configure autologin! Please do so manually.")
+ }
+}
+
+// installFont is skipped for Linux.
+func installFont() {
+ info("Skipping font install for Linux...")
+}
+
+// installService for Linux installs and starts the CSSClient init.d service.
+func installService() {
+ info("Installing service...")
+ shellCommand("cp " + dirPath + "misc/dev/CSSClient /etc/init.d/")
+ shellCommand("chmod +x /etc/init.d/CSSClient")
+ shellCommand("systemctl enable CSSClient")
+ shellCommand("systemctl start CSSClient")
+}
+
+// cleanUp for Linux is primarily focused on removing cached files, history,
+// and other pieces of forensic evidence. It also removes the non-required
+// files in the aeacus directory.
+func cleanUp() {
+ findPaths := "/bin /etc /home /opt /root /sbin /srv /usr /mnt /var"
+
+ info("Changing perms to 755 in " + dirPath + "...")
+ shellCommand("chmod 755 -R " + dirPath)
+
+ info("Removing aeacus binary...")
+ shellCommand("rm " + dirPath + "aeacus")
+
+ info("Removing scoring.conf...")
+ shellCommand("rm " + dirPath + "scoring.conf*")
+
+ info("Removing other setup files...")
+ shellCommand("rm -rf " + dirPath + "misc/")
+ shellCommand("rm -rf " + dirPath + "ReadMe.conf")
+ shellCommand("rm -rf " + dirPath + "README.md")
+ shellCommand("rm -rf " + dirPath + ".git")
+ shellCommand("rm -rf " + dirPath + ".github")
+ shellCommand("rm -rf " + dirPath + "*.go")
+ shellCommand("rm -rf " + dirPath + "Makefile")
+ shellCommand("rm -rf " + dirPath + "go.*")
+ shellCommand("rm -rf " + dirPath + "*.exe")
+ shellCommand("rm -rf " + dirPath + "docs")
+
+ if !ask("Do you want to remove cache and log files, overwrite timestamps, and remove other forensic data from this machine? This may impact data used for your forensic questions!") {
+ return
+ }
+
+ info("Removing .viminfo and .swp files...")
+ shellCommand("find " + findPaths + " -iname '*.viminfo*' -delete -iname '*.swp' -delete")
+
+ info("Symlinking .bash_history and .zsh_history to /dev/null...")
+ shellCommand(`find ` + findPaths + ` -iname '*.bash_history' -exec ln -sf /dev/null {} \;`)
+ shellCommand(`find ` + findPaths + ` -name '.zsh_history' -exec ln -sf /dev/null {} \;`)
+
+ info("Removing .mysql_history...")
+ shellCommand(`find ` + findPaths + ` -name '.mysql_history' -exec rm {} \;`)
+
+ info("Removing .local files...")
+ shellCommand("rm -rf /root/.local /home/*/.local/")
+
+ info("Removing cache...")
+ shellCommand("rm -rf /root/.cache /home/*/.cache/")
+
+ info("Removing temp root and Desktop files...")
+ shellCommand("rm -rf /root/*~ /home/*/Desktop/*~")
+
+ info("Removing crash and VMWare data...")
+ shellCommand("rm -f /var/VMwareDnD/* /var/crash/*.crash")
+
+ info("Removing apt and dpkg logs...")
+ shellCommand("rm -rf /var/log/apt/* /var/log/dpkg.log")
+
+ info("Removing logs (auth and syslog)...")
+ shellCommand("rm -f /var/log/auth.log* /var/log/syslog*")
+
+ info("Removing initial package list...")
+ shellCommand("rm -f /var/log/installer/initial-status.gz")
+
+ info("Installing BleachBit...")
+ shellCommand("apt-get install -y bleachbit")
+
+ info("Clearing Firefox cache and browsing history...")
+ shellCommand("bleachbit --clean firefox.url_history; bleachbit --clean firefox.cache")
+
+ info("Overwriting timestamps to obfuscate changes...")
+ shellCommand(`find /etc /home /var -exec touch --date='2012-12-12 12:12' {} \; 2>/dev/null`)
+}
diff --git a/cmd/release_windows.go b/release_windows.go
similarity index 69%
rename from cmd/release_windows.go
rename to release_windows.go
index 98852ea9..2f91092a 100644
--- a/cmd/release_windows.go
+++ b/release_windows.go
@@ -1,34 +1,34 @@
-package cmd
+package main
-// WriteDesktopFiles writes default scoring engine files to the desktop
-func WriteDesktopFiles() {
+// writeDesktopFiles writes default scoring engine files to the desktop.
+func writeDesktopFiles() {
firefoxBinary := `C:\Program Files\Mozilla Firefox\firefox.exe`
- infoPrint("Writing ScoringReport.html shortcut to Desktop...")
- cmdString := `$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("C:\Users\` + mc.Config.User + `\Desktop\ScoringReport.lnk"); $Shortcut.TargetPath = "` + firefoxBinary + `"; $Shortcut.Arguments = "C:\aeacus\assets\ScoringReport.html"; $Shortcut.Save()`
+ info("Writing ScoringReport.html shortcut to Desktop...")
+ cmdString := `$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("C:\Users\` + conf.User + `\Desktop\ScoringReport.lnk"); $Shortcut.TargetPath = "` + firefoxBinary + `"; $Shortcut.Arguments = "C:\aeacus\assets\ScoringReport.html"; $Shortcut.Save()`
shellCommand(cmdString)
- infoPrint("Writing ReadMe.html shortcut to Desktop...")
- cmdString = `$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("C:\Users\` + mc.Config.User + `\Desktop\ReadMe.lnk"); $Shortcut.TargetPath = "` + firefoxBinary + `"; $Shortcut.Arguments = "C:\aeacus\assets\ReadMe.html"; $Shortcut.Save()`
+ info("Writing ReadMe.html shortcut to Desktop...")
+ cmdString = `$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("C:\Users\` + conf.User + `\Desktop\ReadMe.lnk"); $Shortcut.TargetPath = "` + firefoxBinary + `"; $Shortcut.Arguments = "C:\aeacus\assets\ReadMe.html"; $Shortcut.Save()`
shellCommand(cmdString)
- infoPrint("Creating or emptying TeamID.txt file...")
+ info("Creating or emptying TeamID.txt file...")
cmdString = "echo 'YOUR-TEAMID-HERE' > C:\\aeacus\\TeamID.txt"
shellCommand(cmdString)
- infoPrint("Changing Permissions of TeamID...")
+ info("Changing Permissions of TeamID...")
powershellPermission := `
$ACL = Get-ACL C:\aeacus\TeamID.txt
$ACL.SetOwner([System.Security.Principal.NTAccount] $env:USERNAME)
Set-Acl -Path C:\aeacus\TeamID.txt -AclObject $ACL
`
shellCommand(powershellPermission)
- infoPrint("Writing TeamID shortcut to Desktop...")
- cmdString = `$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("C:\Users\` + mc.Config.User + `\Desktop\TeamID.lnk"); $Shortcut.TargetPath = "C:\aeacus\phocus.exe"; $Shortcut.Arguments = "-i yes"; $Shortcut.Save()`
+ info("Writing TeamID shortcut to Desktop...")
+ cmdString = `$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("C:\Users\` + conf.User + `\Desktop\TeamID.lnk"); $Shortcut.TargetPath = "C:\aeacus\phocus.exe"; $Shortcut.Arguments = "-i yes"; $Shortcut.Save()`
shellCommand(cmdString)
// domain compatibility? doubt
}
-// ConfigureAutologin allows the current user to log in automatically
-func ConfigureAutologin() {
- infoPrint("Setting Up autologin for " + mc.Config.User + "...")
+// configureAutologin allows the current user to log in automatically.
+func configureAutologin() {
+ info("Setting Up autologin for " + conf.User + "...")
powershellAutoLogin := `
function Test-RegistryValue {
@@ -73,9 +73,9 @@ func ConfigureAutologin() {
shellCommand(powershellAutoLogin)
}
-// InstallFont installs the Raleway font for ID Prompt
-func InstallFont() {
- infoPrint("Installing Raleway font for ID Prompt...")
+// installFont installs the Raleway font for ID Prompt.
+func installFont() {
+ info("Installing Raleway font for ID Prompt...")
powershellFontInstall := `
$SourceDir = "C:\aeacus\assets\fonts\Raleway"
$Source = "C:\aeacus\assets\fonts\Raleway\*"
@@ -106,15 +106,15 @@ func InstallFont() {
shellCommand(powershellFontInstall)
}
-// InstallService installs the Aeacus service on Windows
-func InstallService() {
- infoPrint("Installing service with sc.exe...")
+// installService installs the Aeacus service on Windows.
+func installService() {
+ info("Installing service with sc.exe...")
cmdString := `sc.exe create CSSClient binPath= "C:\aeacus\phocus.exe" start= "auto" DisplayName= "CSSClient"`
shellCommand(cmdString)
- infoPrint("Setting service description...")
+ info("Setting service description...")
cmdString = `sc.exe description CSSClient "This is Aeacus's Competition Scoring System client. Don't stop or mess with this unless you want to not get points, and maybe have your registry deleted."`
shellCommand(cmdString)
- infoPrint("Setting up TeamID scheduled task...")
+ info("Setting up TeamID scheduled task...")
idTaskCreate := `
$action = New-ScheduledTaskAction -Execute "C:\aeacus\phocus.exe" -Argument "-i yes"
$trigger = New-ScheduledTaskTrigger -AtLogon
@@ -131,22 +131,22 @@ func InstallService() {
shellCommand(serviceTaskCreate)
}
-// CleanUp clears out sensitive files left behind by
-// image developers or the scoring engine itself
-func CleanUp() {
- infoPrint("Removing .keys file...")
- removeKeys(WindowsDir)
-
- infoPrint("Removing scoring.conf and ReadMe.conf...")
+// cleanUp clears out sensitive files left behind by image developers or the
+// scoring engine.
+func cleanUp() {
+ info("Removing scoring.conf and ReadMe.conf...")
shellCommand("Remove-Item -Force C:\\aeacus\\scoring.conf")
shellCommand("Remove-Item -Force C:\\aeacus\\ReadMe.conf")
- infoPrint("Removing previous.txt...")
+ info("Removing previous.txt...")
shellCommand("Remove-Item -Force C:\\aeacus\\previous.txt")
- infoPrint("Emptying recycle bin...")
+ if !ask("Do you want to remove cache and history files from this machine?") {
+ return
+ }
+ info("Emptying recycle bin...")
shellCommand("Clear-RecycleBin -Force")
- infoPrint("Clearing recently used...")
+ info("Clearing recently used...")
shellCommand("Remove-Item -Force '${env:USERPROFILE}\\AppData\\Roaming\\Microsoft\\Windows\\Recent*.lnk'")
- infoPrint("Clearing run.exe command history...")
+ info("Clearing run.exe command history...")
clearRunScript := `$path = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\RunMRU"
$arr = (Get-Item -Path $path).Property
foreach($item in $arr)
@@ -157,7 +157,7 @@ func CleanUp() {
}
}`
shellCommand(clearRunScript)
- infoPrint("Removing Command History for Powershell")
+ info("Removing Command History for Powershell")
shellCommand("Remove-Item (Get-PSReadlineOption).HistorySavePath")
- warnPrint("Done with automatic cleanup! You need to remove aeacus.exe manually. The only things you need in the C:\\aeacus directory is phocus, scoring.dat, TeamID.txt, and the assets directory.")
+ warn("Done with automatic cleanup! You need to remove aeacus.exe manually. The only things you need in the C:\\aeacus directory is phocus, scoring.dat, TeamID.txt, and the assets directory.")
}
diff --git a/cmd/remote.go b/remote.go
similarity index 51%
rename from cmd/remote.go
rename to remote.go
index 3ca0dfbd..ae800d80 100644
--- a/cmd/remote.go
+++ b/remote.go
@@ -1,4 +1,4 @@
-package cmd
+package main
import (
"crypto/aes"
@@ -18,7 +18,8 @@ import (
"time"
)
-var delimiter = "|-S#-|"
+// Use non-ASCII bytes as a delimiter.
+var delimiter = string(byte(255)) + string(byte(222))
const (
FAIL = "FAIL"
@@ -27,38 +28,29 @@ const (
)
func readTeamID() {
- fileContent, err := readFile(mc.DirPath + "TeamID.txt")
+ fileContent, err := readFile(dirPath + "TeamID.txt")
fileContent = strings.TrimSpace(fileContent)
if err != nil {
- failPrint("TeamID.txt does not exist!")
+ if conf.Remote != "" {
+ fail("TeamID.txt does not exist!")
+ conn.OverallColor = RED
+ conn.OverallStatus = "Your TeamID files does not exist! Failed to score image."
+ conn.Status = false
+ } else {
+ warn("TeamID.txt does not exist! This image is local only, so we will continue.")
+ }
sendNotification("TeamID.txt does not exist!")
- mc.Conn.OverallColor = RED
- mc.Conn.OverallStatus = "Your TeamID files does not exist! Failed to upload scores."
- mc.Connection = false
} else if fileContent == "" {
- failPrint("TeamID.txt is empty!")
+ fail("TeamID.txt is empty!")
sendNotification("TeamID.txt is empty!")
- mc.Conn.OverallStatus = RED
- mc.Conn.OverallStatus = "Your TeamID is empty! Failed to upload scores."
- mc.Connection = false
+ if conf.Remote != "" {
+ conn.OverallStatus = RED
+ conn.OverallStatus = "Your TeamID is empty! Failed to score image."
+ conn.Status = false
+ }
} else {
- mc.TeamID = fileContent
- }
-}
-
-// genChallenge generates a crypto challenge for the CSS endpoint
-func genChallenge() (string, error) {
- // Should actually use this for something
- randomHash1 := "71844fd161e20dc78ce6c985b42611cfb11cf196"
- randomHash2 := "e31ad5a009753ef6da499f961edf0ab3a8eb6e5f"
- chalString := hexEncode(xor(randomHash1, randomHash2))
- hasher := sha256.New()
- _, err := hasher.Write([]byte(mc.Config.Password))
- if err != nil {
- return "", err
+ teamID = fileContent
}
- key := hexEncode(string(hasher.Sum(nil)))
- return hexEncode(xor(key, chalString)), nil
}
func writeString(stringToWrite *strings.Builder, key, value string) {
@@ -71,23 +63,20 @@ func writeString(stringToWrite *strings.Builder, key, value string) {
func genUpdate() (string, error) {
var update strings.Builder
// Write values for score update
- writeString(&update, "team", mc.TeamID)
- writeString(&update, "image", mc.Config.Name)
- writeString(&update, "score", strconv.Itoa(mc.Image.Score))
- chall, err := genChallenge()
- if err != nil {
- return "", err
- }
- writeString(&update, "challenge", chall)
+ writeString(&update, "team", teamID)
+ writeString(&update, "image", conf.Name)
+ writeString(&update, "score", strconv.Itoa(image.Score))
writeString(&update, "vulns", genVulns())
writeString(&update, "time", strconv.Itoa(int(time.Now().Unix())))
- infoPrint("Encrypting score update...")
- if err := deobfuscateData(&mc.Config.Password); err != nil {
- errorPrint(err)
+ info("Encrypting score update...")
+ if err := deobfuscateData(&conf.Password); err != nil {
+ fail(err)
+ return "", err
}
- finishedUpdate := hexEncode(encryptString(mc.Config.Password, update.String()))
- if err := obfuscateData(&mc.Config.Password); err != nil {
- errorPrint(err)
+ finishedUpdate := hexEncode(encryptString(conf.Password, update.String()))
+ if err := obfuscateData(&conf.Password); err != nil {
+ fail(err)
+ return "", err
}
return finishedUpdate, nil
}
@@ -96,59 +85,59 @@ func genVulns() string {
var vulnString strings.Builder
// Vulns achieved
- vulnString.WriteString(fmt.Sprintf("%d%s", len(mc.Image.Points), delimiter))
+ vulnString.WriteString(fmt.Sprintf("%d%s", len(image.Points), delimiter))
// Total vulns
- vulnString.WriteString(fmt.Sprintf("%d%s", mc.Image.ScoredVulns, delimiter))
+ vulnString.WriteString(fmt.Sprintf("%d%s", image.ScoredVulns, delimiter))
// Build vuln string
- for _, penalty := range mc.Image.Penalties {
+ for _, penalty := range image.Penalties {
if err := deobfuscateData(&penalty.Message); err != nil {
- errorPrint(err)
+ fail(err)
}
vulnString.WriteString(fmt.Sprintf("%s - N%.0f pts", penalty.Message, math.Abs(float64(penalty.Points))))
if err := obfuscateData(&penalty.Message); err != nil {
- errorPrint(err)
+ fail(err)
}
vulnString.WriteString(delimiter)
}
- for _, point := range mc.Image.Points {
+ for _, point := range image.Points {
if err := deobfuscateData(&point.Message); err != nil {
- errorPrint(err)
+ fail(err)
}
vulnString.WriteString(fmt.Sprintf("%s - %d pts", point.Message, point.Points))
if err := obfuscateData(&point.Message); err != nil {
- errorPrint(err)
+ fail(err)
}
vulnString.WriteString(delimiter)
}
- infoPrint("Encrypting vulnerabilities...")
+ info("Encrypting vulnerabilities...")
- deobfuscateData(&mc.Config.Password)
- finishedVulns := hexEncode(encryptString(mc.Config.Password, vulnString.String()))
- obfuscateData(&mc.Config.Password)
+ deobfuscateData(&conf.Password)
+ finishedVulns := hexEncode(encryptString(conf.Password, vulnString.String()))
+ obfuscateData(&conf.Password)
return finishedVulns
}
func reportScore() error {
update, err := genUpdate()
if err != nil {
- failPrint(err.Error())
+ fail(err.Error())
return err
}
- resp, err := http.PostForm(mc.Config.Remote+"/update",
+ resp, err := http.PostForm(conf.Remote+"/update",
url.Values{"update": {update}})
if err != nil {
- failPrint(err.Error())
+ fail(err.Error())
return err
}
if resp.StatusCode != 200 {
- mc.Conn.OverallColor = RED
- mc.Conn.OverallStatus = "Failed to upload score! Please ensure that your Team ID is correct."
- mc.Connection = false
- failPrint("Failed to upload score!")
+ conn.OverallColor = RED
+ conn.OverallStatus = "Failed to upload score! Please ensure that your Team ID is correct."
+ conn.Status = false
+ fail("Failed to upload score!")
sendNotification("Failed to upload score!")
return errors.New("Non-200 response from remote scoring endpoint")
}
@@ -157,103 +146,90 @@ func reportScore() error {
func checkServer() {
// Internet check (requisite)
- infoPrint("Checking for internet connection...")
+ info("Checking for internet connection...")
+ // Poor example.org :(
client := http.Client{
- Timeout: 5 * time.Second,
+ Timeout: 10 * time.Second,
}
_, err := client.Get("http://example.org")
if err != nil {
- mc.Conn.NetColor = RED
- mc.Conn.NetStatus = FAIL
+ conn.NetColor = RED
+ conn.NetStatus = FAIL
} else {
- mc.Conn.NetColor = GREEN
- mc.Conn.NetStatus = "OK"
+ conn.NetColor = GREEN
+ conn.NetStatus = "OK"
}
// Scoring engine check
- infoPrint("Checking for scoring engine connection...")
- resp, err := client.Get(mc.Config.Remote + "/status/" + mc.TeamID + "/" + mc.Config.Name)
- // todo enforce status/time limit
- // grab body or status message from minos
- // if "DESTROY" due to image elapsed time > time_limit,
- // destroy image
+ info("Checking for scoring engine connection...")
+ resp, err := client.Get(conf.Remote + "/status/" + teamID + "/" + conf.Name)
if err != nil {
- mc.Conn.ServerColor = RED
- mc.Conn.ServerStatus = FAIL
+ conn.ServerColor = RED
+ conn.ServerStatus = FAIL
} else {
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
- failPrint("Error reading Status body.")
- mc.Conn.ServerColor = RED
- mc.Conn.ServerStatus = FAIL
+ fail("Error reading Status body.")
+ conn.ServerColor = RED
+ conn.ServerStatus = FAIL
} else {
- handleStatus(string(body))
if resp.StatusCode == 200 {
- mc.Conn.ServerColor = GREEN
- mc.Conn.ServerStatus = "OK"
+ conn.ServerColor = GREEN
+ conn.ServerStatus = "OK"
} else {
- mc.Conn.ServerColor = RED
- mc.Conn.ServerStatus = "ERROR"
+ conn.ServerColor = RED
+ conn.ServerStatus = "ERROR"
}
+ handleStatus(string(body))
}
}
// Overall
- if mc.Conn.NetStatus == FAIL && mc.Conn.ServerStatus == "OK" {
+ if conn.NetStatus == FAIL && conn.ServerStatus == "OK" {
timeStart = time.Now()
- mc.Conn.OverallColor = "goldenrod"
- mc.Conn.OverallStatus = "Server connection good but no Internet. Assuming you're on an isolated LAN."
- mc.Connection = true
- } else if mc.Conn.ServerStatus == FAIL {
+ conn.OverallColor = "goldenrod"
+ conn.OverallStatus = "Server connection good but no Internet. Assuming you're on an isolated LAN."
+ conn.Status = true
+ } else if conn.ServerStatus == FAIL {
timeStart = time.Now()
- mc.Conn.OverallColor = RED
- mc.Conn.OverallStatus = "Failure! Can't access remote scoring server."
- failPrint("Can't access remote scoring server!")
+ conn.OverallColor = RED
+ conn.OverallStatus = "Failure! Can't access remote scoring server."
+ fail("Can't access remote scoring server!")
sendNotification("Score upload failure! Unable to access remote server.")
- mc.Connection = false
- } else if mc.Conn.ServerStatus == "ERROR" {
+ conn.Status = false
+ } else if conn.ServerStatus == "ERROR" {
timeWithoutID = time.Since(timeStart)
- if !mc.Config.NoDestroy && timeWithoutID > withoutIDThreshold {
- failPrint("Destroying the image! Too long without inputting valid ID.")
- // destroyImage()
- }
- mc.Conn.OverallColor = RED
- mc.Conn.OverallStatus = "Scoring engine rejected your TeamID!"
- failPrint("Remote server returned an error for its status! Your ID is probably wrong.")
+ conn.OverallColor = RED
+ conn.OverallStatus = "Scoring engine rejected your TeamID!"
+ fail("Remote server returned an error for its status! Your ID is probably wrong.")
sendNotification("Status check failed, TeamID incorrect!")
- mc.Connection = false
- } else if mc.Conn.ServerStatus == "DISABLED" {
- mc.Conn.OverallColor = RED
- mc.Conn.OverallStatus = "Remote scoring server is no longer accepting scores."
- failPrint("Remote scoring server is no longer accepting scores.")
+ conn.Status = false
+ } else if conn.ServerStatus == "DISABLED" {
+ conn.OverallColor = RED
+ conn.OverallStatus = "Remote scoring server is no longer accepting scores."
+ fail("Remote scoring server is no longer accepting scores.")
sendNotification("Remote scoring server is no longer accepting scores.")
- mc.Connection = false
+ conn.Status = false
} else {
timeStart = time.Now()
- mc.Conn.OverallColor = GREEN
- mc.Conn.OverallStatus = "OK"
- mc.Connection = true
+ conn.OverallColor = GREEN
+ conn.OverallStatus = "OK"
+ conn.Status = true
}
}
func handleStatus(status string) {
var statusStruct statusRes
if err := json.Unmarshal([]byte(status), &statusStruct); err != nil {
- failPrint("Failed to parse JSON response (status): " + err.Error())
+ fail("Failed to parse JSON response (status): " + err.Error())
}
-
switch statusStruct.Status {
- case "DIE":
- failPrint("Destroying image! Server has told me to die.")
- // destroyImage()
- case "GIMMESHELL":
- if !mc.Config.DisableShell && !mc.ShellActive {
- go connectWs()
- }
+ case "DISABLED":
+ conn.ServerStatus = "DISABLED"
}
}
@@ -268,7 +244,8 @@ func encryptString(password, plainText string) string {
hasher := sha256.New()
_, err := hasher.Write([]byte(password))
if err != nil {
- errorPrint(err)
+ fail(err)
+ return ""
}
key := hasher.Sum(nil)
@@ -279,25 +256,29 @@ func encryptString(password, plainText string) string {
}
plainText = plainText + string(paddingArray)
if len(plainText)%aes.BlockSize != 0 {
- panic("plainText is not a multiple of block size!")
+ fail("plainText is not a multiple of block size!")
+ return ""
}
// Create cipher block with key.
block, err := aes.NewCipher(key)
if err != nil {
- panic(err)
+ fail(err)
+ return ""
}
// Generate nonce.
nonce := make([]byte, 12)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
- panic(err.Error())
+ fail(err)
+ return ""
}
// Create NewGCM cipher.
aesgcm, err := cipher.NewGCM(block)
if err != nil {
- panic(err.Error())
+ fail(err)
+ return ""
}
// Encrypt and seal plainText.
@@ -313,7 +294,7 @@ func decryptString(password, ciphertext string) string {
// Create a sha256sum hash of the password provided.
hasher := sha256.New()
if _, err := hasher.Write([]byte(password)); err != nil {
- errorPrint(err)
+ fail(err)
}
key := hasher.Sum(nil)
@@ -324,21 +305,21 @@ func decryptString(password, ciphertext string) string {
// Create the AES block object.
block, err := aes.NewCipher(key)
if err != nil {
- failPrint(err.Error())
+ fail(err.Error())
return ""
}
// Create the AES-GCM cipher with the generated block.
aesgcm, err := cipher.NewGCM(block)
if err != nil {
- failPrint(err.Error())
+ fail(err.Error())
return ""
}
// Decrypt (and check validity, since it's GCM) of ciphertext.
plainText, err := aesgcm.Open(nil, iv, []byte(ciphertext), nil)
if err != nil {
- failPrint(err.Error())
+ fail(err.Error())
return ""
}
diff --git a/score.go b/score.go
new file mode 100644
index 00000000..e934dd62
--- /dev/null
+++ b/score.go
@@ -0,0 +1,275 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "os"
+ "strconv"
+)
+
+var (
+ teamID string
+ conf = &config{}
+ image = &imageData{}
+ conn = &connData{}
+)
+
+// imageData is the current scoring data for the image. It is able to be
+// wiped, removed, etc, on each run without affecting anything else.
+type imageData struct {
+ Contribs int
+ Detracts int
+ Score int
+ ScoredVulns int
+ TotalPoints int
+ Penalties []scoreItem
+ Points []scoreItem
+}
+
+// connData represents the current connectivity state of the image to the
+// internet and the scoring server.
+type connData struct {
+ Status bool
+ OverallColor string
+ OverallStatus string
+ NetColor string
+ NetStatus string
+ ServerColor string
+ ServerStatus string
+}
+
+// scoreItem is the scoring report representation of a check, containing only
+// the message and points associated with it.
+type scoreItem struct {
+ Message string
+ Points int
+}
+
+// config is a representation of the TOML configuration typically
+// specific in scoring.conf.
+type config struct {
+ Local bool
+ Shell bool
+ EndDate string
+ Name string
+ OS string
+ Password string
+ Remote string
+ Title string
+ User string
+ Version string
+ Check []check
+}
+
+// statusRes is to parse a JSON response from the remote server.
+type statusRes struct {
+ Status string `json:"status"`
+}
+
+// ReadScoringData is a convenience function around readData and decodeString,
+// which parses the encrypted scoring configuration file.
+func readScoringData() error {
+ info("Decrypting data from " + dirPath + scoringData + "...")
+ // Read in the encrypted configuration file
+ dataFile, err := readFile(dirPath + scoringData)
+ if err != nil {
+ return err
+ } else if dataFile == "" {
+ return errors.New("Scoring data is empty!")
+ }
+ decryptedData, err := decryptConfig(dataFile)
+ if err != nil {
+ return err
+ }
+ if err != nil {
+ fail("Error reading in scoring data: " + err.Error())
+ return err
+ } else if decryptedData == "" {
+ fail("Scoring data is empty! Is the file corrupted?")
+ return errors.New("Scoring data is empty!")
+ } else {
+ info("Data decryption successful!")
+ }
+ parseConfig(decryptedData)
+ return nil
+}
+
+// ScoreImage is the main function for scoring the image.
+func scoreImage() {
+ checkTrace()
+ if timeCheck() {
+ log.Fatal("Image is running outside of the specified end date.")
+ }
+ info("Scoring image...")
+
+ // Ensure checks aren't blank, and grab TeamID.
+ checkConfigData()
+
+ // If local is enabled, we want to:
+ // 1. Score checks
+ // 2. Check if server is up (if remote)
+ // 3. If connection, report score
+ // 4. Generate report
+ if conf.Local {
+ scoreChecks()
+ if conf.Remote != "" {
+ checkServer()
+ if conn.Status {
+ err := reportScore()
+ if err != nil {
+ fail(err)
+ }
+ }
+ }
+ genReport(image)
+ } else {
+ // If local is disabled, we want to:
+ // 1. Check if server is up
+ // 2. If no connection, generate report with err text
+ // 3. If connection, score checks
+ // 4. Report the score
+ // 5. If reporting failed, show error, wipe scoring data
+ // 6. Generate report
+ checkServer()
+ if !conn.Status {
+ warn("Connection failed-- generating blank report.")
+ genReport(image)
+ return
+ }
+ scoreChecks()
+ err := reportScore()
+ if err != nil {
+ image = &imageData{}
+ warn("Local is disabled, scoring data removed.")
+ }
+ genReport(image)
+ }
+
+ // Check if points increased/decreased.
+ prevPoints, err := readFile(dirPath + "assets/previous.txt")
+ if err == nil {
+ prevScore, err := strconv.Atoi(prevPoints)
+ if err != nil {
+ fail("Don't mess with previous.txt! It only helps us know when to play sound and send notifications.")
+ } else {
+ if prevScore < image.Score {
+ sendNotification("You gained points!")
+ playAudio(dirPath + "assets/wav/gain.wav")
+ } else if prevScore > image.Score {
+ sendNotification("You lost points!")
+ playAudio(dirPath + "assets/wav/alarm.wav")
+ }
+ }
+ } else if os.IsExist(err) {
+ fail("Reading from previous.txt failed!")
+ }
+
+ // Write previous.txt from current round.
+ writeFile(dirPath+"assets/previous.txt", strconv.Itoa(image.Score))
+
+ // Remove imageData for next scoring round.
+ image = &imageData{}
+}
+
+// checkConfigData performs preliminary checks on the configuration data, reads
+// in the TeamID, and autogenerates missing values.
+func checkConfigData() {
+ if len(conf.Check) == 0 {
+ conn.OverallColor = "red"
+ conn.OverallStatus = "There were no checks found in the configuration."
+ } else {
+ // For none-remote local connections
+ conn.OverallColor = "green"
+ conn.OverallStatus = "OK"
+ conn.Status = true
+ }
+
+ readTeamID()
+}
+
+// scoreChecks runs through every check configured.
+func scoreChecks() {
+ for _, check := range conf.Check {
+ scoreCheck(check)
+ }
+ info(fmt.Sprintf("Score: %d", image.Score))
+}
+
+// scoreCheck will go through each condition inside a check, and determine
+// whether or not the check passes.
+func scoreCheck(check check) {
+ status := false
+ failed := false
+
+ // If a fail condition passes, the check fails, no other checks required.
+ if len(check.Fail) > 0 {
+ failed = checkFails(&check)
+ }
+
+ // If a PassOverride succeeds, that overrides the Pass checks
+ if !failed && len(check.PassOverride) > 0 {
+ status = checkPassOverrides(&check)
+ }
+
+ // Finally, we check the normal pass checks
+ if !failed && !status && len(check.Pass) > 0 {
+ status = checkPass(&check)
+ }
+
+ if status {
+ if check.Points >= 0 {
+ if verboseEnabled {
+ deobfuscateData(&check.Message)
+ pass(fmt.Sprintf("Check passed: %s - %d pts", check.Message, check.Points))
+ obfuscateData(&check.Message)
+ }
+ image.Points = append(image.Points, scoreItem{check.Message, check.Points})
+ image.Contribs += check.Points
+ } else {
+ if verboseEnabled {
+ deobfuscateData(&check.Message)
+ fail(fmt.Sprintf("Penalty triggered: %s - %d pts", check.Message, check.Points))
+ obfuscateData(&check.Message)
+ }
+ image.Penalties = append(image.Penalties, scoreItem{check.Message, check.Points})
+ image.Detracts += check.Points
+ }
+ image.Score += check.Points
+ }
+
+ // If check is not a penalty, add to total
+ if check.Points >= 0 {
+ image.ScoredVulns++
+ image.TotalPoints += check.Points
+ }
+}
+
+func checkFails(check *check) bool {
+ for _, cond := range check.Fail {
+ failStatus := runCheck(cond)
+ if failStatus {
+ return true
+ }
+ }
+ return false
+}
+
+func checkPassOverrides(check *check) bool {
+ for _, cond := range check.PassOverride {
+ status := runCheck(cond)
+ if status {
+ return true
+ }
+ }
+ return false
+}
+
+func checkPass(check *check) bool {
+ for _, cond := range check.Pass {
+ if !runCheck(cond) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/shell_linux.go b/shell_linux.go
new file mode 100644
index 00000000..1ef1ef0a
--- /dev/null
+++ b/shell_linux.go
@@ -0,0 +1,102 @@
+package main
+
+import (
+ "net/url"
+ "os"
+ "os/exec"
+ "strings"
+ "time"
+
+ "github.com/creack/pty"
+ "github.com/gorilla/websocket"
+)
+
+func shellSocket() {
+ var disconnected bool
+ remoteURL, _ := url.Parse(conf.Remote)
+
+ readTeamID()
+ curTeamID := string(teamID)
+ u := url.URL{Scheme: "ws", Host: remoteURL.Host, Path: "/ws/" + curTeamID + "-" + conf.Name}
+
+ c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
+ if err != nil {
+ disconnected = true
+ } else {
+ disconnected = false
+ if err := c.WriteMessage(websocket.TextMessage, []byte("WRITER")); err != nil {
+ disconnected = true
+ }
+ }
+ defer c.Close()
+
+ cmd := exec.Command("bash")
+ cmd.Env = append(os.Environ(), "TERM=xterm")
+ term, _ := pty.Start(cmd)
+
+ go func() {
+ for {
+ if !disconnected {
+ buf := make([]byte, 512)
+ _, err := term.Read(buf)
+ if err != nil {
+ cmd = exec.Command("bash")
+ cmd.Env = append(os.Environ(), "TERM=xterm")
+ term, _ = pty.Start(cmd)
+
+ continue
+ }
+
+ if err := c.WriteMessage(websocket.TextMessage, buf); err != nil {
+ disconnected = true
+ continue
+ }
+ }
+ }
+ }()
+
+ ticker := time.NewTicker(2 * time.Second)
+ defer ticker.Stop()
+
+ for {
+
+ if !disconnected {
+ _, message, err := c.ReadMessage()
+
+ if err != nil {
+ if strings.Contains(err.Error(), "1006") {
+ disconnected = true
+ }
+ }
+
+ _, err = term.Write([]byte(message))
+ if err != nil {
+ cmd = exec.Command("bash")
+ cmd.Env = append(os.Environ(), "TERM=xterm")
+ term, _ = pty.Start(cmd)
+
+ continue
+ }
+ } else {
+ select {
+ case <-ticker.C:
+ if disconnected {
+ readTeamID()
+ curTeamID := string(teamID)
+ u = url.URL{Scheme: "ws", Host: remoteURL.Host, Path: "/ws/" + curTeamID + "-" + conf.Name}
+
+ c, _, err = websocket.DefaultDialer.Dial(u.String(), nil)
+ if err != nil {
+ disconnected = true
+ } else {
+ disconnected = false
+ if err := c.WriteMessage(websocket.TextMessage, []byte("WRITER")); err != nil {
+ disconnected = true
+ }
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/shell_windows.go b/shell_windows.go
new file mode 100644
index 00000000..204ab242
--- /dev/null
+++ b/shell_windows.go
@@ -0,0 +1,113 @@
+package main
+
+import (
+ "net/url"
+ "os"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/ActiveState/termtest/conpty"
+ "github.com/gorilla/websocket"
+)
+
+func shellSocket() {
+ cpty, _ := conpty.New(100, 50)
+ var disconnected bool
+ remoteURL, _ := url.Parse(conf.Remote)
+
+ readTeamID()
+ curTeamID := string(teamID)
+ u := url.URL{Scheme: "ws", Host: remoteURL.Host, Path: "/ws/" + curTeamID + "-" + conf.Name}
+
+ c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
+ if err != nil {
+ disconnected = true
+ } else {
+ disconnected = false
+ if err := c.WriteMessage(websocket.TextMessage, []byte("WRITER")); err != nil {
+ disconnected = true
+ }
+ }
+ defer c.Close()
+
+ cpty.Spawn(
+ "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
+ []string{},
+ &syscall.ProcAttr{
+ Env: os.Environ(),
+ },
+ )
+
+ go func() {
+ for {
+ if !disconnected {
+ buf := make([]byte, 512)
+ _, err := cpty.OutPipe().Read(buf)
+ if err != nil {
+ cpty.Spawn(
+ "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
+ []string{},
+ &syscall.ProcAttr{
+ Env: os.Environ(),
+ },
+ )
+ continue
+ }
+
+ if err := c.WriteMessage(websocket.TextMessage, buf); err != nil {
+ disconnected = true
+ continue
+ }
+ }
+ }
+ }()
+
+ ticker := time.NewTicker(2 * time.Second)
+ defer ticker.Stop()
+
+ for {
+
+ if !disconnected {
+ _, message, err := c.ReadMessage()
+
+ if err != nil {
+ if strings.Contains(err.Error(), "1006") {
+ disconnected = true
+ }
+ }
+
+ _, err = cpty.Write([]byte(message))
+ if err != nil {
+ cpty.Spawn(
+ "C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
+ []string{},
+ &syscall.ProcAttr{
+ Env: os.Environ(),
+ },
+ )
+ continue
+ }
+ } else {
+ select {
+ case <-ticker.C:
+ if disconnected {
+ readTeamID()
+ curTeamID := string(teamID)
+ u = url.URL{Scheme: "ws", Host: remoteURL.Host, Path: "/ws/" + curTeamID + "-" + conf.Name}
+
+ c, _, err = websocket.DefaultDialer.Dial(u.String(), nil)
+ if err != nil {
+ disconnected = true
+ } else {
+ disconnected = false
+ if err := c.WriteMessage(websocket.TextMessage, []byte("WRITER")); err != nil {
+ disconnected = true
+ }
+ }
+ }
+ }
+ }
+ }
+
+}
diff --git a/utility.go b/utility.go
new file mode 100644
index 00000000..a58cb212
--- /dev/null
+++ b/utility.go
@@ -0,0 +1,328 @@
+package main
+
+import (
+ "errors"
+ "io/ioutil"
+ "os"
+ "runtime"
+ "strings"
+ "time"
+)
+
+const (
+ version = "2.0.0"
+)
+
+var (
+ yesEnabled bool
+ verboseEnabled bool
+ debugEnabled bool
+ dirPath string
+ scoringConf = "scoring.conf"
+ scoringData = "scoring.dat"
+)
+
+var (
+ timeStart = time.Now()
+ timeWithoutID, _ = time.ParseDuration("0s")
+ withoutIDThreshold, _ = time.ParseDuration("30m")
+)
+
+const (
+ shellCmdLen = 15
+)
+
+// determineDirectory sets the dirPath variable based on its environment.
+func determineDirectory() error {
+ if dirPath == "" {
+ if runtime.GOOS == "linux" {
+ dirPath = "/opt/aeacus/"
+ } else if runtime.GOOS == "windows" {
+ dirPath = `C:\aeacus\`
+ } else {
+ fail("Unknown OS (" + runtime.GOOS + "): you need to specify an aeacus directory")
+ return errors.New("unknown OS: " + runtime.GOOS)
+ }
+ } else if dirPath[len(dirPath)-1] != '\\' && dirPath[len(dirPath)-1] != '/' {
+ return errors.New("Your scoring directory must end in a slash: " + dirPath + "/")
+ }
+ return nil
+}
+
+// timeCheck determines if an image is being used within the intended
+// competition time slot. This is easy to spoof, and is not meant to be a
+// security feature.
+func timeCheck() bool {
+ if conf.EndDate != "" {
+ date, err := time.Parse("2006/01/02 15:04:05 MST", conf.EndDate)
+ if err != nil {
+ fail("Your EndDate value in the configuration is invalid: " + err.Error())
+ } else {
+ if time.Now().After(date) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// writeFile wraps ioutil's WriteFile function, and prints
+// the error the screen if one occurs.
+func writeFile(fileName, fileContent string) {
+ err := ioutil.WriteFile(fileName, []byte(fileContent), 0o644)
+ if err != nil {
+ fail("Error writing file: " + err.Error())
+ }
+}
+
+// PermsCheck is a convenience function wrapper around
+// adminCheck, which prints an error indicating that admin
+// permissions are needed.
+func permsCheck() {
+ if !adminCheck() {
+ fail("You need to run this binary as root or Administrator!")
+ os.Exit(1)
+ }
+}
+
+// shellCommand executes a given command in a shell environment.
+func shellCommand(commandGiven string) error {
+ cmd := rawCmd(commandGiven)
+ if err := cmd.Run(); err != nil {
+ if verboseEnabled {
+ if len(commandGiven) > shellCmdLen {
+ fail("Command \"" + commandGiven[:shellCmdLen] + "...\" errored out (code " + err.Error() + ").")
+ } else {
+ fail("Command \"" + commandGiven + "\" errored out (code " + err.Error() + ").")
+ }
+ }
+ return err
+ }
+ return nil
+}
+
+// shellCommandOutput executes a given command in a shell environment and
+// returns its output.
+func shellCommandOutput(commandGiven string) (string, error) {
+ out, err := rawCmd(commandGiven).Output()
+ if err != nil {
+ if verboseEnabled {
+ if len(commandGiven) > shellCmdLen {
+ fail("Command \"" + commandGiven[:shellCmdLen] + "...\" errored out (code " + err.Error() + ").")
+ } else {
+ fail("Command \"" + commandGiven + "\" errored out (code " + err.Error() + ").")
+ }
+ }
+ return "", err
+ }
+ return string(out), err
+}
+
+// assignPoints is used to automatically assign points to checks that don't
+// have a hardcoded points value.
+func assignPoints() {
+ pointlessChecks := []int{}
+
+ for i, check := range conf.Check {
+ if check.Points == 0 {
+ pointlessChecks = append(pointlessChecks, i)
+ } else if check.Points > 0 {
+ image.TotalPoints += check.Points
+ }
+ }
+
+ pointsLeft := 100 - image.TotalPoints
+ if pointsLeft <= 0 && len(pointlessChecks) > 0 || len(pointlessChecks) > 100 {
+ // If the specified points already value over 100, yet there are checks
+ // without points assigned, we assign the default point value of 3
+ // (arbitrarily chosen).
+ for _, check := range pointlessChecks {
+ conf.Check[check].Points = 3
+ }
+ } else if pointsLeft > 0 && len(pointlessChecks) > 0 {
+ pointsEach := pointsLeft / len(pointlessChecks)
+ for _, check := range pointlessChecks {
+ conf.Check[check].Points = pointsEach
+ }
+ image.TotalPoints += (pointsEach * len(pointlessChecks))
+ if image.TotalPoints < 100 {
+ for i := 0; image.TotalPoints < 100; image.TotalPoints++ {
+ conf.Check[pointlessChecks[i]].Points++
+ i++
+ if i > len(pointlessChecks)-1 {
+ i = 0
+ }
+ }
+ image.TotalPoints += (100 - image.TotalPoints)
+ }
+ }
+
+ // Reset TotalPoints, since it was only used as a scratch variable and will
+ // be calculated again when the checks are run.
+ image.TotalPoints = 0
+}
+
+// assignDescriptions is automatically assign descriptions to checks that don't
+// have one.
+func assignDescriptions() {
+ for i, check := range conf.Check {
+ var msg string
+ if check.Message != "" {
+ continue
+ }
+ for _, cond := range check.Pass {
+ if msg != "" {
+ newMsg := getDesc(cond)
+ if newMsg != "" {
+ msg += ", and " + strings.ToLower(string(newMsg[0])) + newMsg[1:]
+ }
+ } else {
+ msg = getDesc(cond)
+ }
+ }
+ for _, cond := range check.PassOverride {
+ if msg != "" {
+ newMsg := getDesc(cond)
+ if newMsg != "" {
+ msg += ", OR " + strings.ToLower(string(newMsg[0])) + newMsg[1:]
+ }
+ } else {
+ msg = getDesc(cond)
+ }
+ }
+ if msg == "" {
+ msg = "Check passed"
+ }
+ conf.Check[i].Message = msg
+ }
+}
+
+func getDesc(c cond) string {
+ switch c.Type {
+ case "Command":
+ return "Command \"" + c.Cmd + "\" passed"
+ case "CommandNot":
+ return "Command \"" + c.Cmd + "\" failed"
+ case "CommandOutput":
+ return "Command \"" + c.Cmd + "\" had the output \"" + c.Value + "\""
+ case "CommandOutputNot":
+ return "Command \"" + c.Cmd + "\" did not have the output \"" + c.Value + "\""
+ case "CommandContains":
+ return "command \"" + c.Cmd + "\" contained output \"" + c.Value + "\""
+ case "CommandContainsNot":
+ return "Command \"" + c.Cmd + "\" output did not contain \"" + c.Value + "\""
+ case "PathExists":
+ return "Path \"" + c.Path + "\" exists"
+ case "PathExistsNot":
+ return "Path \"" + c.Path + "\" does not exist"
+ case "FileContains":
+ return "File \"" + c.Path + "\" contains regular expression \"" + c.Value + "\""
+ case "FileContainsNot":
+ return "File \"" + c.Path + "\" does not contain regular expression \"" + c.Value + "\""
+ case "DirContains":
+ return "Directory \"" + c.Path + "\" contains expression \"" + c.Value + "\""
+ case "DirContainsNot":
+ return "Directory \"" + c.Path + "\" does not contain expression \"" + c.Value + "\""
+ case "FileEquals":
+ return "File \"" + c.Path + "\" matches hash"
+ case "FileEqualsNot":
+ return "File \"" + c.Path + "\" doesn't match hash"
+ case "ProgramInstalled":
+ return c.Name + " is installed"
+ case "ProgramInstalledNot":
+ return c.Name + " has been removed"
+ case "ServiceUp":
+ return "Service \"" + c.Name + "\" is installed and running"
+ case "ServiceUpNot":
+ return "Service " + c.Name + " has been stopped"
+ case "UserExists":
+ return "User " + c.User + " has been added"
+ case "UserExistsNot":
+ return "User " + c.User + " has been removed"
+ case "UserInGroup":
+ return "User " + c.User + " is in group \"" + c.Group + "\""
+ case "UserInGroupNot":
+ return "User " + c.User + " removed or is not in group \"" + c.Group + "\""
+ case "FirewallUp":
+ return "Firewall has been enabled"
+ case "FirewallUpNot":
+ return "Firewall has been disabled"
+ case "ProgramVersion":
+ return c.Name + " is version " + c.Value
+ case "ProgramVersionNot":
+ return c.Name + " is not version " + c.Value
+
+ // Linux checks
+ case "AutoCheckUpdatesEnabled":
+ return "The system automatically checks for updates daily"
+ case "AutoCheckUpdatesEnabledNot":
+ return "The system does not automatically checks for updates daily"
+ case "GuestDisabledLDM":
+ return "Guest is disabled"
+ case "GuestDisabledLDMNot":
+ return "Guest is enabled"
+ case "KernelVersion":
+ return "Kernel is version " + c.Value
+ case "KernelVersionNot":
+ return "Kernel is not version " + c.Value
+ case "PermissionIs":
+ return "Permissions of " + c.Path + " are " + c.Value
+ case "PermissionIsNot":
+ return "Permissions of " + c.Path + " are not " + c.Value
+
+ // Windows checks
+ case "BitlockerEnabled":
+ return "Bitlocker drive encryption has been enabled"
+ case "BitlockerEnabledNot":
+ return "Bitlocker drive encryption has been disabled"
+ case "FileOwner":
+ return c.Path + " is owned by " + c.User
+ case "FileOwnerNot":
+ return c.Path + " is not owned by " + c.User
+ case "PasswordChanged":
+ return "Password for " + c.User + " has been changed"
+ case "PasswordChangedNot":
+ return "Password for " + c.User + " has not been changed"
+ case "SecurityPolicy":
+ return "Security policy option " + c.Key + " is set to " + c.Value
+ case "SecurityPolicyNot":
+ return "Security policy option " + c.Key + " is not set to " + c.Value
+ case "ServiceStartup":
+ return c.Name + " has startup type " + c.Value
+ case "ServiceStartupNot":
+ return c.Name + " does not have startup type " + c.Value
+ case "ScheduledTaskExists":
+ return "Scheduled task " + c.Name + " exists"
+ case "ScheduledTaskExistsNot":
+ return "Scheduled task " + c.Name + " doesn't exist"
+ case "ShareExists":
+ return "Share " + c.Name + " exists"
+ case "ShareExistsNot":
+ return "Share " + c.Name + " doesn't exist"
+ case "RegistryKey":
+ return "Registry key " + c.Key + " matches \"" + c.Value + "\""
+ case "RegistryKeyNot":
+ return "Registry key " + c.Key + " does not match \"" + c.Value + "\""
+ case "RegistryKeyExists":
+ return "Registry key " + c.Key + " exists"
+ case "RegistryKeyExistsNot":
+ return "Registry key " + c.Key + " does not exist"
+ case "UserDetail":
+ return "User property " + c.Key + " for " + c.User + " is equal to \"" + c.Value + "\""
+ case "UserDetailNot":
+ return "User property " + c.Key + " for " + c.User + " is not equal to \"" + c.Value + "\""
+ case "UserRights":
+ return "User or group " + c.User + " has privilege \"" + c.Value + "\""
+ case "UserRightsNot":
+ return "User or group " + c.User + " does not have privilege \"" + c.Value + "\""
+ case "WindowsFeature":
+ return c.Name + " feature has been enabled"
+ case "WindowsFeatureNot":
+ return c.Name + " feature has been disabled"
+
+ default:
+ warn("Cannot autogenerate message for check type:", c.Type)
+ return ""
+ }
+
+}
diff --git a/utility_linux.go b/utility_linux.go
new file mode 100644
index 00000000..4c5c1811
--- /dev/null
+++ b/utility_linux.go
@@ -0,0 +1,112 @@
+package main
+
+import (
+ "crypto/md5"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "os/user"
+ "strconv"
+)
+
+// readFile (Linux) wraps ioutil's ReadFile function.
+func readFile(fileName string) (string, error) {
+ fileContent, err := ioutil.ReadFile(fileName)
+ return string(fileContent), err
+}
+
+// decodeString (linux) strictly does nothing, however it's here
+// for compatibility with Windows ANSI/UNICODE/etc.
+func decodeString(fileContent string) (string, error) {
+ return fileContent, nil
+}
+
+// sendNotification sends a notification to the end user.
+func sendNotification(messageString string) {
+ if conf.User == "" {
+ fail("User not specified in configuration, can't send notification.")
+ } else {
+ err := shellCommand(`
+ user="` + conf.User + `"
+ uid="$(id -u $user)" # Ubuntu >= 18
+ if [ -e /run/user/$uid/bus ]; then
+ display="unix:path=/run/user/$uid/bus"
+ else # Ubuntu <= 16
+ display="unix:abstract=$(cat /run/user/$uid/dbus-session | cut -d '=' -f3)"
+ fi
+ sudo -u $user DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=$display notify-send -i ` + dirPath + `assets/img/logo.png "Aeacus SE" "` + messageString + `"`)
+ if err != nil {
+ fail("Sending notification failed. Is the user in the configuration correct, and are they logged in to a desktop environment?")
+ }
+ }
+}
+
+func checkTrace() {
+ result, err := cond{
+ Path: "/proc/self/status",
+ Value: `^TracerPid:\s+0$`,
+ }.FileContains()
+
+ // If there was an error reading the file, the user may be restricting access to /proc for the phocus binary
+ // through tools such as AppArmor. In this case, the engine should error out.
+ if !result || err != nil {
+ fail("Try harder instead of ptracing the engine, please.")
+ os.Exit(1)
+ }
+}
+
+// createFQs is a quality of life function that creates Forensic Question files
+// on the Desktop, pre-populated with a template.
+func CreateFQs(numFqs int) {
+ for i := 1; i <= numFqs; i++ {
+ fileName := "'Forensic Question " + strconv.Itoa(i) + ".txt'"
+ shellCommand("echo 'QUESTION:' > /home/" + conf.User + "/Desktop/" + fileName)
+ shellCommand("echo 'ANSWER:' >> /home/" + conf.User + "/Desktop/" + fileName)
+ info("Wrote " + fileName + " to Desktop")
+ }
+}
+
+// rawCmd returns a exec.Command object for Linux shell commands.
+func rawCmd(commandGiven string) *exec.Cmd {
+ return exec.Command("/bin/sh", "-c", commandGiven)
+}
+
+// playAudio plays a .wav file with the given path.
+func playAudio(wavPath string) {
+ info("Playing audio:", wavPath)
+ commandText := "aplay " + wavPath
+ shellCommand(commandText)
+}
+
+// hashFileMD5 generates the MD5 Hash of a file with the given path.
+func hashFileMD5(filePath string) (string, error) {
+ var returnMD5String string
+ file, err := os.Open(filePath)
+ if err != nil {
+ return returnMD5String, err
+ }
+ defer file.Close()
+ hash := md5.New()
+ if _, err := io.Copy(hash, file); err != nil {
+ return returnMD5String, err
+ }
+ hashInBytes := hash.Sum(nil)[:16]
+ return hexEncode(string(hashInBytes)), err
+}
+
+func adminCheck() bool {
+ currentUser, err := user.Current()
+ uid, _ := strconv.Atoi(currentUser.Uid)
+ if err != nil {
+ fail("Error for checking if running as root: " + err.Error())
+ return false
+ } else if uid != 0 {
+ return false
+ }
+ return true
+}
+
+func getInfo(infoType string) {
+ warn("Info gathering is not supported for Linux-- there's always a better, easier command-line tool.")
+}
diff --git a/cmd/utility_windows.go b/utility_windows.go
similarity index 65%
rename from cmd/utility_windows.go
rename to utility_windows.go
index 89700b8c..f43ae376 100644
--- a/cmd/utility_windows.go
+++ b/utility_windows.go
@@ -1,4 +1,4 @@
-package cmd
+package main
import (
"bytes"
@@ -6,7 +6,6 @@ import (
"io/ioutil"
"os"
"os/exec"
- "strconv"
"strings"
"github.com/gen2brain/beeep"
@@ -34,7 +33,7 @@ func readFile(filename string) (string, error) {
// decodeString (Windows) attempts to determine the file encoding type
// (typically, UTF-8, UTF-16, or ANSI) and return the appropriately
-// encoded string.
+// encoded string. (HACK)
func decodeString(fileContent string) (string, error) {
// If contains ~>40% null bytes, we're gonna assume its Unicode
raw := []byte(fileContent)
@@ -56,6 +55,7 @@ func decodeString(fileContent string) (string, error) {
// Make an tranformer that converts MS-Win default to UTF8
win16be := unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM)
+
// Make a transformer that is like win16be, but abides by BOM
utf16bom := unicode.BOMOverride(win16be.NewDecoder())
@@ -67,20 +67,22 @@ func decodeString(fileContent string) (string, error) {
return string(decoded), err
}
+// checkTrace runs WinAPI function "IsDebuggerPresent" to check for an attached
+// debugger.
func checkTrace() {
result, _, _ := debuggerCheck.Call()
if int(result) != 0 {
- failPrint("Reversing is cool, but we would appreciate if you practiced your skills in an environment that was less destructive to other peoples' experiences.")
+ fail("Reversing is cool, but we would appreciate if you practiced your skills in an environment that was less destructive to other peoples' experiences.")
os.Exit(1)
}
}
-// sendNotification (Windows) employes the beeep library to send notifications
+// sendNotification (Windows) employs the beeep library to send notifications
// to the end user.
func sendNotification(messageString string) {
- err := beeep.Notify("Aeacus SE", messageString, mc.DirPath+"assets/img/logo.png")
+ err := beeep.Notify("Aeacus SE", messageString, dirPath+"assets/img/logo.png")
if err != nil {
- failPrint("Notification error: " + err.Error())
+ fail("Notification error: " + err.Error())
}
}
@@ -91,56 +93,17 @@ func sendNotification(messageString string) {
// system and retrieve the return value.
func rawCmd(commandGiven string) *exec.Cmd {
cmdInput := "powershell.exe -NonInteractive -NoProfile Invoke-Command -ScriptBlock { " + commandGiven + " }"
- debugPrint("rawCmd input: " + cmdInput)
+ debug("rawCmd input: " + cmdInput)
return exec.Command("powershell.exe", "-NonInteractive", "-NoProfile", "Invoke-Command", "-ScriptBlock", "{ "+commandGiven+" }")
}
-// shellCommand (Windows) executes a given command in a PowerShell environment
-// and prints an error if one occurred.
-func shellCommand(commandGiven string) {
- cmd := rawCmd(commandGiven)
- if err := cmd.Run(); err != nil {
- if _, ok := err.(*exec.ExitError); ok {
- if len(commandGiven) > 12 {
- failPrint("Command \"" + commandGiven[:12] + "...\" errored out (code " + err.Error() + ").")
- } else {
- failPrint("Command \"" + commandGiven + "\" errored out (code " + err.Error() + ").")
- }
- }
- }
-}
-
-// shellCommand (Windows) executes a given command in a PowerShell environment
-// and returns the commands output and its error (if one occurred).
-func shellCommandOutput(commandGiven string) (string, error) {
- out, err := rawCmd(commandGiven).Output()
- if err != nil {
- if len(commandGiven) > 9 {
- failPrint("Command \"" + commandGiven[:9] + "...\" errored out (code " + err.Error() + ").")
- } else {
- failPrint("Command \"" + commandGiven + "\" errored out (code " + err.Error() + ").")
- }
- return "", err
- }
- return strings.TrimSpace(string(out)), err
-}
-
+// playAudio plays a .wav file with the given path with PowerShell.
func playAudio(wavPath string) {
+ info("Playing audio:", wavPath)
commandText := "(New-Object Media.SoundPlayer '" + wavPath + "').PlaySync();"
shellCommand(commandText)
}
-// CreateFQs is a quality of life function that creates Forensic Question files
-// on the Desktop, pre-populated with a template.
-func CreateFQs(numFqs int) {
- for i := 1; i <= numFqs; i++ {
- fileName := "'Forensic Question " + strconv.Itoa(i) + ".txt'"
- shellCommand("echo 'QUESTION:' > C:\\Users\\" + mc.Config.User + "\\Desktop\\" + fileName)
- shellCommand("echo 'ANSWER:' >> C:\\Users\\" + mc.Config.User + "\\Desktop\\" + fileName)
- infoPrint("Wrote " + fileName + " to Desktop")
- }
-}
-
// adminCheck (Windows) will attempt to open:
// \\.\PHYSICALDRIVE0
// and will return true if this succeeds, which means the process is running
@@ -150,26 +113,8 @@ func adminCheck() bool {
return err == nil
}
-func destroyImage() {
- failPrint("Destroying the image!")
- if verboseEnabled {
- warnPrint("Since you're running this in verbose mode, I assume you're a developer who messed something up. You've been spared from image deletion but please be careful.")
- } else {
- shellCommand("del /s /q C:\\aeacus")
- if !mc.Config.NoDestroy {
- // nuke registry
- // other destructive commands
- // rm -rf /
- // kill all procceses
- // overwrite system32
- shellCommand("shutdown /r /t 0")
- }
- os.Exit(1)
- }
-}
-
-// sidToLocalUser takes an SID as a string and returns a string containing
-// the username of the Local User (NTAccount) that it belongs to.
+// sidToLocalUser takes an SID as a string and returns a string containing the
+// username of the Local User (NTAccount) that it belongs to.
func sidToLocalUser(sid string) string {
cmdText := "$objSID = New-Object System.Security.Principal.SecurityIdentifier('" + sid + "'); $objUser = $objSID.Translate([System.Security.Principal.NTAccount]); Write-Host $objUser.Value"
output, _ := shellCommandOutput(cmdText)
@@ -202,7 +147,7 @@ func getPrograms() ([]string, error) {
softwareList := []string{}
sw, err := wapi.InstalledSoftwareList()
if err != nil {
- failPrint("Couldn't get programs: " + err.Error())
+ fail("Couldn't get programs: " + err.Error())
return softwareList, err
}
for _, s := range sw {
@@ -211,14 +156,13 @@ func getPrograms() ([]string, error) {
return softwareList, nil
}
-// getProgram returns the Software struct of program data from a name.
-// The first Program that contains the substring passed as the programName
-// is returned.
+// getProgram returns the Software struct of program data from a name. The first
+// Program that contains the substring passed as the programName is returned.
func getProgram(programName string) (shared.Software, error) {
prog := shared.Software{}
sw, err := wapi.InstalledSoftwareList()
if err != nil {
- failPrint("Couldn't get programs: " + err.Error())
+ fail("Couldn't get programs: " + err.Error())
}
for _, s := range sw {
if strings.Contains(s.Name(), programName) {
@@ -231,7 +175,7 @@ func getProgram(programName string) (shared.Software, error) {
func getLocalUsers() ([]shared.LocalUser, error) {
ul, err := wapi.ListLocalUsers()
if err != nil {
- failPrint("Couldn't get local users: " + err.Error())
+ fail("Couldn't get local users: " + err.Error())
}
return ul, err
}
@@ -239,7 +183,7 @@ func getLocalUsers() ([]shared.LocalUser, error) {
func getLocalAdmins() ([]shared.LocalUser, error) {
ul, err := wapi.ListLocalUsers()
if err != nil {
- failPrint("Couldn't get local users: " + err.Error())
+ fail("Couldn't get local users: " + err.Error())
}
var admins []shared.LocalUser
for _, user := range ul {
@@ -267,7 +211,7 @@ func getLocalServiceStatus(serviceName string) (shared.Service, error) {
serviceDataList, err := wapi.GetServices()
var serviceStatusData shared.Service
if err != nil {
- failPrint("Couldn't get local service: " + err.Error())
+ fail("Couldn't get local service: " + err.Error())
return serviceStatusData, err
}
for _, v := range serviceDataList {
@@ -275,6 +219,6 @@ func getLocalServiceStatus(serviceName string) (shared.Service, error) {
return v, nil
}
}
- failPrint(`Specified service '` + serviceName + `' was not found on the system`)
+ fail(`Specified service '` + serviceName + `' was not found on the system`)
return serviceStatusData, err
}
diff --git a/cmd/web.go b/web.go
similarity index 81%
rename from cmd/web.go
rename to web.go
index c18f68be..b00246ce 100644
--- a/cmd/web.go
+++ b/web.go
@@ -1,4 +1,4 @@
-package cmd
+package main
import (
"fmt"
@@ -9,38 +9,43 @@ import (
"time"
)
-func genReport(img imageData) {
- teamID := mc.TeamID
+func genReport(img *imageData) {
+
+ // displayTeamID is used for the tiling background.
+ var displayTeamID string
if len(teamID) < 7 {
- teamID = "1010 1101"
+ displayTeamID = "1010 1101"
+ } else {
+ displayTeamID = teamID
}
- header := ` Aeacus Scoring Report ![](./img/logo.png)
`
+
+ header := `
Aeacus Scoring Report ![](./img/logo.png)
`
footer := `
The Aeacus project is free and open source software. This project is in no way endorsed or affiliated with the Air Force Association or the University of Texas at San Antonio.
`
var htmlFile strings.Builder
htmlFile.WriteString(header)
genTime := time.Now()
- htmlFile.WriteString("
" + mc.Config.Title + "
")
+ htmlFile.WriteString("
" + conf.Title + "
")
htmlFile.WriteString("
Report Generated At: " + genTime.Format("2006/01/02 15:04:05 MST") + "
")
htmlFile.WriteString(``)
- if mc.Config.Remote != "" {
- htmlFile.WriteString(`
Current Team ID: ` + mc.TeamID + `
`)
+ if conf.Remote != "" {
+ htmlFile.WriteString(`
Current Team ID: ` + teamID + `
`)
}
htmlFile.WriteString(fmt.Sprintf(`
%d out of %d points received
`, img.Score, img.TotalPoints))
- if mc.Config.Remote != "" {
- htmlFile.WriteString(`
Click here to view the public scoreboard`)
- htmlFile.WriteString(`
Click here to view the announcements`)
+ if conf.Remote != "" {
+ htmlFile.WriteString(`
Click here to view the public scoreboard`)
+ htmlFile.WriteString(`
Click here to view the announcements`)
- htmlFile.WriteString(`
Connection Status: ` + mc.Conn.OverallStatus + `
`)
+ htmlFile.WriteString(`
Connection Status: ` + conn.OverallStatus + `
`)
- htmlFile.WriteString(`Internet Connectivity Check:
` + mc.Conn.NetStatus + ``)
- htmlFile.WriteString(`Aeacus Server Connection Status:
` + mc.Conn.ServerStatus + ``)
+ htmlFile.WriteString(`Internet Connectivity Check:
` + conn.NetStatus + ``)
+ htmlFile.WriteString(`Aeacus Server Connection Status:
` + conn.ServerStatus + ``)
} else {
- htmlFile.WriteString(`
Connection Status: ` + mc.Conn.OverallStatus + `
`)
+ htmlFile.WriteString(`
Connection Status: ` + conn.OverallStatus + `
`)
htmlFile.WriteString(`Internet Connectivity Check:
N/A`)
htmlFile.WriteString(`Aeacus Server Connection Status:
N/A`)
}
@@ -65,13 +70,13 @@ func genReport(img imageData) {
htmlFile.WriteString(footer)
- infoPrint("Writing HTML to ScoringReport.html...")
- writeFile(mc.DirPath+"assets/ScoringReport.html", htmlFile.String())
+ info("Writing HTML to ScoringReport.html...")
+ writeFile(dirPath+"assets/ScoringReport.html", htmlFile.String())
}
-// GenReadMe generates a competition ReadMe with some built-in defaults from your
-// ReadMe.conf
-func GenReadMe() {
+// genReadMe generates a competition ReadMe with some built-in defaults from
+// your ReadMe.conf.
+func genReadMe() {
header := `
@@ -135,7 +140,7 @@ func GenReadMe() {
-
+
`
@@ -181,14 +186,14 @@ func GenReadMe() {
var htmlFile strings.Builder
htmlFile.WriteString(header)
- htmlFile.WriteString("
" + mc.Config.OS + " " + mc.Config.Title + " README
")
+ htmlFile.WriteString("
" + conf.OS + " " + conf.Title + " README
")
htmlFile.WriteString(headerTheSequel)
- htmlFile.WriteString("
" + mc.Config.OS + "
")
+ htmlFile.WriteString("
" + conf.OS + "
")
htmlFile.WriteString(`
- It is company policy to use only ` + mc.Config.OS + ` on this computer. It is also company policy to use only the
- latest, official, stable ` + mc.Config.OS + ` packages available for required software and services on this computer.
+ It is company policy to use only ` + conf.OS + ` on this computer. It is also company policy to use only the
+ latest, official, stable ` + conf.OS + ` packages available for required software and services on this computer.
Management has decided that the default web browser for all users on this computer should be the latest stable
version of Firefox.`)
@@ -198,13 +203,24 @@ func GenReadMe() {
}
htmlFile.WriteString("
")
- userReadMe, err := readFile("ReadMe.conf")
+
+ // Check for common variations of ReadMe.conf.
+ var userReadMe string
+ var err error
+ readMeFiles := []string{"ReadMe.conf", "README.conf", "readme.conf"}
+ for _, readme := range readMeFiles {
+ userReadMe, err = readFile(readme)
+ if err == nil {
+ break
+ }
+ }
if err != nil {
- failPrint("No ReadMe.conf file found!")
+ fail("No ReadMe.conf file found!")
os.Exit(1)
}
+
htmlFile.WriteString(userReadMe)
htmlFile.WriteString(footer)
- infoPrint("Writing HTML to ReadMe.html...")
- writeFile(mc.DirPath+"assets/ReadMe.html", htmlFile.String())
+ info("Writing HTML to ReadMe.html...")
+ writeFile(dirPath+"assets/ReadMe.html", htmlFile.String())
}