├── version.go ├── .gitignore ├── include ├── logo.png └── logo.txt ├── util.go ├── printer.go ├── LICENSE ├── go.mod ├── download-atomics.sh ├── types └── atomic-test.go ├── Makefile ├── art.go ├── README.md ├── go.sum ├── cmd └── main.go └── executor.go /version.go: -------------------------------------------------------------------------------- 1 | package atomicredteam 2 | 3 | var Version = "version not set" 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .devcontainer 2 | 3 | bin 4 | include/atomics 5 | include/custom 6 | .vscode/* 7 | -------------------------------------------------------------------------------- /include/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/activeshadow/go-atomicredteam/HEAD/include/logo.png -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package atomicredteam 2 | 3 | import "strings" 4 | 5 | func ExpandStringSlice(s []string) []string { 6 | if len(s) == 0 { 7 | return nil 8 | } 9 | 10 | var r []string 11 | 12 | for _, e := range s { 13 | t := strings.Split(e, ",") 14 | r = append(r, t...) 15 | } 16 | 17 | return r 18 | } 19 | -------------------------------------------------------------------------------- /include/logo.txt: -------------------------------------------------------------------------------- 1 | ___ __ _ ____ __ ______ 2 | / | / /_____ ____ ___ (_)____ / __ \___ ____/ / /_ __/__ ____ _____ ___ 3 | / /| |/ __/ __ \/ __ `__ \/ / ___/ / /_/ / _ \/ __ / / / / _ \/ __ `/ __ `__ \ 4 | / ___ / /_/ /_/ / / / / / / / /__ / _, _/ __/ /_/ / / / / __/ /_/ / / / / / / 5 | /_/ |_\__/\____/_/ /_/ /_/_/\___/ /_/ |_|\___/\__,_/ /_/ \___/\__,_/_/ /_/ /_/ 6 | -------------------------------------------------------------------------------- /printer.go: -------------------------------------------------------------------------------- 1 | package atomicredteam 2 | 3 | import "fmt" 4 | 5 | var Quiet bool 6 | 7 | func Println(a ...interface{}) (int, error) { 8 | if Quiet { 9 | return 0, nil 10 | } 11 | 12 | return fmt.Println(a...) 13 | } 14 | 15 | func Printf(format string, a ...interface{}) (int, error) { 16 | if Quiet { 17 | return 0, nil 18 | } 19 | 20 | return fmt.Printf(format, a...) 21 | } 22 | 23 | func Print(a ...interface{}) (int, error) { 24 | if Quiet { 25 | return 0, nil 26 | } 27 | 28 | return fmt.Print(a...) 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bryan Richardson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module actshad.dev/go-atomicredteam 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/charmbracelet/glamour v0.2.0 7 | github.com/muesli/termenv v0.6.0 8 | github.com/urfave/cli/v2 v2.2.0 9 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 11 | ) 12 | 13 | require ( 14 | github.com/alecthomas/chroma v0.7.3 // indirect 15 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect 16 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect 17 | github.com/dlclark/regexp2 v1.2.0 // indirect 18 | github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f // indirect 19 | github.com/lucasb-eyer/go-colorful v1.0.3 // indirect 20 | github.com/mattn/go-isatty v0.0.12 // indirect 21 | github.com/mattn/go-runewidth v0.0.9 // indirect 22 | github.com/microcosm-cc/bluemonday v1.0.2 // indirect 23 | github.com/muesli/reflow v0.1.0 // indirect 24 | github.com/olekukonko/tablewriter v0.0.4 // indirect 25 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 26 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 27 | github.com/yuin/goldmark v1.2.0 // indirect 28 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /download-atomics.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | usage="usage: $(basename "$0") [-d] [-h] [-v] 4 | 5 | Download latest version of atomic-red-team atomic tests from the given 6 | repository owner and branch (defaults to redcanaryco/master). This script 7 | assumes the repository name is 'atomic-red-team'. 8 | 9 | This script will exit early if the include/atomics directory already exists 10 | unless the 'force' option is provided. 11 | 12 | where: 13 | -f force download of latest atomics 14 | -h show this help text 15 | -r repo owner and branch to download from" 16 | 17 | 18 | repo=redcanaryco/master 19 | force=false 20 | 21 | 22 | # loop through positional options/arguments 23 | while getopts ':fhr:' option; do 24 | case "$option" in 25 | f) force=true ;; 26 | h) echo -e "$usage"; exit ;; 27 | r) repo="$OPTARG" ;; 28 | \?) echo -e "illegal option: -$OPTARG\n" >$2 29 | echo -e "$usage" >&2 30 | exit 1 ;; 31 | esac 32 | done 33 | 34 | IFS='/' read -ra repo <<< "$repo" 35 | 36 | if (( ${#repo[@]} != 2 )); then 37 | echo "invalid repo provided" 38 | exit 1 39 | fi 40 | 41 | [ -d include/atomics ] && [ "$force" = false ] && echo "\ 42 | Atomics already exist - not overwriting. 43 | Delete include/atomics directory and rerun if you want to reinstall." \ 44 | && exit 0 45 | 46 | url=https://github.com/${repo[0]}/atomic-red-team/archive/${repo[1]}.zip 47 | 48 | echo "Downloading repo archive from $url" 49 | 50 | curl -L -o art.zip $url 51 | 52 | echo "Unarchiving repo" 53 | 54 | unzip art.zip 55 | rm art.zip 56 | 57 | echo "Copying archived atomics to 'include' directory" 58 | 59 | cp -a atomic-red-team-${repo[1]}/atomics include 60 | 61 | echo "Deleting unarchived repo" 62 | 63 | rm -rf atomic-red-team-${repo[1]} 64 | -------------------------------------------------------------------------------- /types/atomic-test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // See https://github.com/redcanaryco/atomic-red-team/blob/master/atomic_red_team/atomic_test_template.yaml 4 | // and https://github.com/redcanaryco/atomic-red-team/blob/master/atomic_red_team/spec.yaml 5 | 6 | var SupportedExecutors = []string{"bash", "command_prompt", "manual", "powershell", "sh"} 7 | 8 | type Atomic struct { 9 | AttackTechnique string `yaml:"attack_technique"` 10 | DisplayName string `yaml:"display_name"` 11 | AtomicTests []AtomicTest `yaml:"atomic_tests"` 12 | 13 | BaseDir string `yaml:"-"` 14 | } 15 | 16 | type AtomicTest struct { 17 | Name string `yaml:"name"` 18 | GUID string `yaml:"auto_generated_guid,omitempty"` 19 | Description string `yaml:"description,omitempty"` 20 | SupportedPlatforms []string `yaml:"supported_platforms"` 21 | 22 | InputArugments map[string]InputArgument `yaml:"input_arguments,omitempty"` 23 | 24 | DependencyExecutorName string `yaml:"dependency_executor_name,omitempty"` 25 | 26 | Dependencies []Dependency `yaml:"dependencies,omitempty"` 27 | Executor *AtomicExecutor `yaml:"executor"` 28 | 29 | BaseDir string `yaml:"-"` 30 | } 31 | 32 | type InputArgument struct { 33 | Description string `yaml:"description"` 34 | Type string `yaml:"type"` 35 | Default string `yaml:"default"` 36 | ExpectedValue string `yaml:"expected_value,omitempty"` 37 | } 38 | 39 | type Dependency struct { 40 | Description string `yaml:"description"` 41 | PrereqCommand string `yaml:"prereq_command,omitempty"` 42 | GetPrereqCommand string `yaml:"get_prereq_command,omitempty"` 43 | } 44 | 45 | type AtomicExecutor struct { 46 | Name string `yaml:"name"` 47 | ElevationRequired bool `yaml:"elevation_required"` 48 | Command string `yaml:"command,omitempty"` 49 | Steps string `yaml:"steps,omitempty"` 50 | CleanupCommand string `yaml:"cleanup_command,omitempty"` 51 | 52 | ExecutedCommand map[string]interface{} `yaml:"executed_command,omitempty"` 53 | } 54 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | # Default version number to the shorthand git commit hash if not set at the 4 | # command line. 5 | VER := $(or $(VER),$(shell git log -1 --format="%h")) 6 | COMMIT := $(shell git log -1 --format="%h - %ae") 7 | DATE := $(shell date -u) 8 | VERSION := $(VER) (commit $(COMMIT)) $(DATE) 9 | 10 | GOSOURCES := $(shell find . \( -name '*.go' \)) 11 | INCLUDES := $(shell find include \( -name '*' \) | sed 's/ /\\ /g') 12 | 13 | # Default atomics repo to redcanaryco/master if not set at the command line. 14 | ATOMICS_REPO := $(or $(ATOMICS_REPO),redcanaryco/master) 15 | 16 | THISFILE := $(lastword $(MAKEFILE_LIST)) 17 | THISDIR := $(shell dirname $(realpath $(THISFILE))) 18 | GOBIN := $(THISDIR)/bin 19 | 20 | # Prepend this repo's bin directory to our path since we'll want to 21 | # install some build tools there for use during the build process. 22 | PATH := $(GOBIN):$(PATH) 23 | 24 | # Export GOBIN env variable so `go install` picks it up correctly. 25 | export GOBIN 26 | 27 | all: 28 | 29 | clean: 30 | -rm bin/goart* 31 | -rm -rf include/atomics 32 | 33 | .PHONY: download-atomics 34 | download-atomics: 35 | ./download-atomics.sh $(ATOMICS_REPO) 36 | 37 | bin/goart: $(GOSOURCES) download-atomics 38 | mkdir -p bin 39 | CGO_ENABLED=0 GOOS=linux go build -a -ldflags="-X 'actshad.dev/go-atomicredteam.Version=$(VERSION)' -X 'actshad.dev/go-atomicredteam.REPO=$(ATOMICS_REPO)' -s -w" -trimpath -o bin/goart cmd/main.go 40 | 41 | bin/goart-darwin: $(GOSOURCES) download-atomics 42 | mkdir -p bin 43 | CGO_ENABLED=0 GOOS=darwin go build -a -ldflags="-X 'actshad.dev/go-atomicredteam.Version=$(VERSION)' -X 'actshad.dev/go-atomicredteam.REPO=$(ATOMICS_REPO)' -s -w" -trimpath -o bin/goart-darwin cmd/main.go 44 | 45 | bin/goart.exe: $(GOSOURCES) download-atomics 46 | mkdir -p bin 47 | CGO_ENABLED=0 GOOS=windows go build -a -ldflags="-X 'actshad.dev/go-atomicredteam.Version=$(VERSION)' -X 'actshad.dev/go-atomicredteam.REPO=$(ATOMICS_REPO)' -s -w" -trimpath -o bin/goart.exe cmd/main.go 48 | 49 | release: bin/goart bin/goart-darwin bin/goart.exe 50 | -------------------------------------------------------------------------------- /art.go: -------------------------------------------------------------------------------- 1 | package atomicredteam 2 | 3 | import ( 4 | "embed" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | LOCAL string 12 | REPO string 13 | BUNDLED bool 14 | TEMPDIR string 15 | 16 | AtomicsFolderRegex = regexp.MustCompile(`PathToAtomicsFolder(\\|\/)`) 17 | BlockQuoteRegex = regexp.MustCompile(`<\/?blockquote>`) 18 | ) 19 | 20 | //go:embed include/* 21 | var include embed.FS 22 | 23 | func Logo() []byte { 24 | logo, err := include.ReadFile("include/logo.txt") 25 | if err != nil { 26 | panic(err) 27 | } 28 | 29 | return logo 30 | } 31 | 32 | func Techniques() []string { 33 | var techniques []string 34 | 35 | entries, err := include.ReadDir("include/atomics") 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | for _, entry := range entries { 41 | if entry.IsDir() && strings.HasPrefix(entry.Name(), "T") { 42 | techniques = append(techniques, entry.Name()) 43 | } 44 | } 45 | 46 | entries, err = include.ReadDir("include/custom") 47 | if err != nil { 48 | return techniques 49 | } 50 | 51 | for _, entry := range entries { 52 | if entry.IsDir() && strings.HasPrefix(entry.Name(), "T") { 53 | techniques = append(techniques, entry.Name()) 54 | } 55 | } 56 | 57 | return techniques 58 | } 59 | 60 | func Technique(tid string) ([]byte, string, error) { 61 | // Check for a custom atomic first, then public. 62 | if body, err := include.ReadFile("include/custom/" + tid + "/" + tid + ".yaml"); err == nil { 63 | return body, "include/custom/", nil 64 | } 65 | 66 | if body, err := include.ReadFile("include/custom/" + tid + "/" + tid + ".yml"); err == nil { 67 | return body, "include/custom/", nil 68 | } 69 | 70 | if body, err := include.ReadFile("include/atomics/" + tid + "/" + tid + ".yaml"); err == nil { 71 | return body, "include/atomics/", nil 72 | } 73 | 74 | if body, err := include.ReadFile("include/atomics/" + tid + "/" + tid + ".yml"); err == nil { 75 | return body, "include/atomics/", nil 76 | } 77 | 78 | return nil, "", fmt.Errorf("Atomic Test is not currently bundled") 79 | } 80 | 81 | func Markdown(tid string) ([]byte, error) { 82 | var ( 83 | body []byte 84 | err error 85 | ) 86 | 87 | // Check for a custom atomic first, then public. 88 | body, err = include.ReadFile("include/custom/" + tid + "/" + tid + ".md") 89 | if err != nil { 90 | body, err = include.ReadFile("include/atomics/" + tid + "/" + tid + ".md") 91 | if err != nil { 92 | return nil, fmt.Errorf("Atomic Test is not currently bundled") 93 | } 94 | } 95 | 96 | return body, nil 97 | } 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 |
4 | 5 | logo 6 |

GoART

7 | 8 |

9 | go-atomicredteam is a Golang application to execute tests as defined in the atomics folder of Red Canary's Atomic Red Team project. 10 |

11 | 12 | 13 | 14 | [![Go Reference](https://pkg.go.dev/badge/github.com/activeshadow/go-atomicredteam.svg)](https://pkg.go.dev/github.com/activeshadow/go-atomicredteam) [![Go Report Card](https://goreportcard.com/badge/github.com/activeshadow/go-atomicredteam?style=flat-square)](https://goreportcard.com/report/github.com/activeshadow/go-atomicredteam) 15 | 16 |
17 | Report Bug 18 | · 19 | Contribute Features 20 |
21 |
22 | 23 |
24 | 25 | 26 | ## :star2: About the Project 27 | 28 | The "atomics folder" contains a folder for each Technique defined by the MITRE ATT&CK™ Framework. Inside of each of these `T#` folders you'll find a yaml file that defines the attack procedures for each atomic test as well as an easier to read markdown (md) version of the same data. 29 | 30 | - Executing atomic tests may leave your system in an undesirable state. You are responsible for understanding what a test does before executing. 31 | - Ensure you have permission to test before you begin. 32 | - It is recommended to set up a test machine for atomic test execution that is similar to the build in your environment. Be sure you have your collection/EDR solution in place, and that the endpoint is checking in and active. 33 | 34 | #### Goals 35 | 36 | The goal of go-atomicredteam (***goart*** for short) is to package all the atomic test definitions and documentation directly into the executable such that it can be used in environments without Internet access. As of now this is not fool proof, however, since some tests themselves include calls to download additional resources from the Internet. 37 | 38 | ***goart*** also supports local development of tests by making it possible for a 39 | user to dump existing test definitions to a local directory and load tests 40 | present in the local directory when executing. If a test is both defined 41 | locally and within the executable, the local test will be used. 42 | 43 | 44 | ### :camera: Screenshots 45 | 46 |

47 | help 48 |

49 | 50 | 51 | ### :dart: Features 52 | 53 | - Standalone Atomic Red Team Executor 54 | - Supports Windows, MacOS, and Linux (assuming it's cross-compiled). 55 | - Makefile to download atomics to build **_goart_** 56 | 57 | 58 | ### :key: Config Variables 59 | 60 | #### Using Assets Provided With An Atomic 61 | 62 | - Sometimes, atomic tests need to bring their own tools or config files with them. 63 | This is supported, and a good example to reference is the atomic for 64 | `T1003.004`. To make use of `PsExec.exe` automatically as referenced in the 65 | test, it would simply need to be added to the `include/atomics/T1003.004/bin` 66 | directory before `go-atomicredteam` is rebuilt. 67 | 68 | - The key is the `PathToAtomicsFolder` string included in the default argument for 69 | the `psexec_exe` input argument. This string will be replaced with the 70 | appropriate path, depending on if it's a default test, a custom test, or a local 71 | test. In this case it's a default test, so `PathToAtomicsFolder` is replaced 72 | with `include/atomics` before trying to access the file. 73 | 74 | 75 | ## :toolbox: Getting Started 76 | 77 | 78 | ### :bangbang: Prerequisites 79 | 80 | This project uses Golang: 81 | 82 | - Following the [install instructions](https://go.dev/doc/install) to install Golang. 83 | - Optional: A test system (e.g. VM, spare laptop, etc.) to run ***goart*** 84 | 85 | 86 | ### :running: Build: Go Package 87 | 88 | To build ***goart*** for all operating systems, simply run `make release` from 89 | the root directory. Otherwise, run the appropriate `make bin/goart-` 90 | target for your OS. 91 | 92 | ![get_started](https://user-images.githubusercontent.com/1636709/172280657-7e8d49a3-28f5-4045-95e8-e19226fc7819.gif) 93 | 94 | 95 | Clone the project 96 | 97 | ```bash 98 | git clone https://github.com/activeshadow/go-atomicredteam 99 | ``` 100 | 101 | Go to the project directory 102 | 103 | ```bash 104 | cd go-atomicredteam 105 | ``` 106 | 107 | Optionally update dependencies if receiving `//go:linkname must refer to declared function or variable` error 108 | 109 | ```bash 110 | go get -u && go mod tidy 111 | go get -u golang.org/x/sys 112 | ``` 113 | 114 | Build ***goart*** 115 | 116 | ```bash 117 | # All operating systems 118 | make release 119 | 120 | # Or target operating system 121 | make bin/goart- 122 | ``` 123 | 124 | 125 | 126 | 127 | ## :eyes: Usage 128 | 129 | ```bash 130 | # Run the goart command 131 | bin/goart --help 132 | ``` 133 | 134 | 135 | ### :test_tube: Running Tests 136 | 137 | ```bash 138 | # run technique by name 139 | bin/goart-darwin --technique T1006 --name "Read volume boot sector via DOS device path (PowerShell)" 140 | 141 | # run technique by index (starts at 0) 142 | bin/goart-darwin --technique T1006 --index 0 143 | ``` 144 | 145 | 146 | ## :warning: License 147 | 148 | Distributed under the MIT License. See [LICENSE](LICENSE) for more information. 149 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= 3 | github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= 4 | github.com/alecthomas/chroma v0.7.3 h1:NfdAERMy+esYQs8OXk0I868/qDxxCEo7FMz1WIqMAeI= 5 | github.com/alecthomas/chroma v0.7.3/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM= 6 | github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= 7 | github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= 8 | github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= 9 | github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= 10 | github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= 11 | github.com/charmbracelet/glamour v0.2.0 h1:mTgaiNiumpqTZp3qVM6DH9UB0NlbY17wejoMf1kM8Pg= 12 | github.com/charmbracelet/glamour v0.2.0/go.mod h1:UA27Kwj3QHialP74iU6C+Gpc8Y7IOAKupeKMLLBURWM= 13 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 14 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 15 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= 16 | github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= 17 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 18 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 19 | github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= 20 | github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 21 | github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4= 22 | github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= 23 | github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= 24 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 25 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 26 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 27 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 28 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 29 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 30 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 31 | github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= 32 | github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= 33 | github.com/muesli/reflow v0.1.0 h1:oQdpLfO56lr5pgLvqD0TcjW85rDjSYSBVdiG1Ch1ddM= 34 | github.com/muesli/reflow v0.1.0/go.mod h1:I9bWAt7QTg/que/qmUCJBGlj7wEq8OAFBjPNjc6xK4I= 35 | github.com/muesli/termenv v0.6.0 h1:zxvzTBmo4ZcxhNGGWeMz+Tttm51eF5bmPjfy4MCRYlk= 36 | github.com/muesli/termenv v0.6.0/go.mod h1:SohX91w6swWA4AYU+QmPx+aSgXhWO0juiyID9UZmbpA= 37 | github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= 38 | github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= 39 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 40 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 41 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 42 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 43 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 44 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 45 | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= 46 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 47 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 48 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 49 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 50 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 51 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 52 | github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= 53 | github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= 54 | github.com/yuin/goldmark v1.2.0 h1:WOOcyaJPlzb8fZ8TloxFe8QZkhOOJx87leDa9MIT9dc= 55 | github.com/yuin/goldmark v1.2.0/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 56 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 57 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 58 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= 59 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 60 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 61 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k= 65 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 66 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 67 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 68 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 69 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 70 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 71 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 72 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "runtime" 9 | "sort" 10 | "strings" 11 | "time" 12 | 13 | art "actshad.dev/go-atomicredteam" 14 | 15 | "github.com/charmbracelet/glamour" 16 | "github.com/muesli/termenv" 17 | "github.com/urfave/cli/v2" 18 | "gopkg.in/yaml.v3" 19 | ) 20 | 21 | func main() { 22 | app := &cli.App{ 23 | Name: "goart", 24 | Usage: "Standalone Atomic Red Team Executor (written in Go)", 25 | Version: art.Version, 26 | Flags: []cli.Flag{ 27 | &cli.StringFlag{ 28 | Name: "technique", 29 | Aliases: []string{"t"}, 30 | Usage: "technique ID", 31 | }, 32 | &cli.StringFlag{ 33 | Name: "name", 34 | Aliases: []string{"n"}, 35 | Usage: "test name", 36 | }, 37 | &cli.IntFlag{ 38 | Name: "index", 39 | Aliases: []string{"i"}, 40 | Usage: "test index", 41 | Value: -1, 42 | }, 43 | &cli.StringSliceFlag{ 44 | Name: "input", 45 | Usage: "input key=value pairs", 46 | }, 47 | &cli.StringSliceFlag{ 48 | Name: "env", 49 | Aliases: []string{"e"}, 50 | Usage: "env variable key=value pairs", 51 | }, 52 | &cli.StringFlag{ 53 | Name: "local-atomics-path", 54 | Aliases: []string{"l"}, 55 | Usage: "directory containing additional/custom atomic test definitions", 56 | }, 57 | &cli.StringFlag{ 58 | Name: "dump-technique", 59 | Aliases: []string{"d"}, 60 | Usage: "directory to dump the given technique test config to", 61 | }, 62 | &cli.StringFlag{ 63 | Name: "results-file", 64 | Aliases: []string{"o"}, 65 | Usage: "file to write test results to (auto-generated by default)", 66 | }, 67 | &cli.StringFlag{ 68 | Name: "results-format", 69 | Aliases: []string{"f"}, 70 | Usage: "format to use when writing results to file (json, yaml)", 71 | Value: "yaml", 72 | }, 73 | &cli.BoolFlag{ 74 | Name: "quiet", 75 | Aliases: []string{"q"}, 76 | Usage: "disable printing info to terminal when executing a test", 77 | }, 78 | }, 79 | Action: func(ctx *cli.Context) error { 80 | if art.REPO == "" { 81 | art.REPO = ctx.String("repo") 82 | } else { 83 | art.BUNDLED = true 84 | } 85 | 86 | if local := ctx.String("local-atomics-path"); local != "" { 87 | art.LOCAL = local 88 | } 89 | 90 | var ( 91 | tid = ctx.String("technique") 92 | name = ctx.String("name") 93 | index = ctx.Int("index") 94 | inputs = art.ExpandStringSlice(ctx.StringSlice("input")) 95 | env = art.ExpandStringSlice(ctx.StringSlice("env")) 96 | ) 97 | 98 | if tid != "" && (name != "" || index != -1) { 99 | // Only honor --quiet flag if actually executing a test. 100 | art.Quiet = ctx.Bool("quiet") 101 | } 102 | 103 | art.Println(string(art.Logo())) 104 | 105 | if name != "" && index != -1 { 106 | return cli.Exit("only provide one of 'name' or 'index' flags", 1) 107 | } 108 | 109 | if tid == "" { 110 | filter := make(map[string]struct{}) 111 | 112 | listTechniques := func() ([]string, error) { 113 | var ( 114 | techniques []string 115 | descriptions []string 116 | ) 117 | 118 | for technique := range filter { 119 | techniques = append(techniques, technique) 120 | } 121 | 122 | sort.Strings(techniques) 123 | 124 | for _, tid := range techniques { 125 | technique, err := art.GetTechnique(tid) 126 | if err != nil { 127 | return nil, fmt.Errorf("unable to get technique %s: %w", tid, err) 128 | } 129 | 130 | descriptions = append(descriptions, fmt.Sprintf("%s - %s", tid, technique.DisplayName)) 131 | } 132 | 133 | return descriptions, nil 134 | } 135 | 136 | getLocalTechniques := func() error { 137 | files, err := ioutil.ReadDir(art.LOCAL) 138 | if err != nil { 139 | return fmt.Errorf("unable to read contents of provided local atomics path: %w", err) 140 | } 141 | 142 | for _, f := range files { 143 | if f.IsDir() && strings.HasPrefix(f.Name(), "T") { 144 | filter[f.Name()] = struct{}{} 145 | } 146 | } 147 | 148 | return nil 149 | } 150 | 151 | if art.BUNDLED { 152 | // Get bundled techniques first. 153 | for _, asset := range art.Techniques() { 154 | filter[asset] = struct{}{} 155 | } 156 | 157 | // We want to get local techniques after getting bundled techniques so 158 | // the local techniques will replace any bundled techniques with the 159 | // same ID. 160 | if art.LOCAL != "" { 161 | if err := getLocalTechniques(); err != nil { 162 | return cli.Exit(err.Error(), 1) 163 | } 164 | } 165 | 166 | descriptions, err := listTechniques() 167 | if err != nil { 168 | cli.Exit(err.Error(), 1) 169 | } 170 | 171 | art.Println("Locally Available Techniques:\n") 172 | 173 | for _, desc := range descriptions { 174 | art.Println(desc) 175 | } 176 | 177 | return nil 178 | } 179 | 180 | // Even if we're not running in bundled mode, still see if the user 181 | // wants to load any local techniques. 182 | if art.LOCAL != "" { 183 | if err := getLocalTechniques(); err != nil { 184 | return cli.Exit(err.Error(), 1) 185 | } 186 | 187 | descriptions, err := listTechniques() 188 | if err != nil { 189 | cli.Exit(err.Error(), 1) 190 | } 191 | 192 | art.Println("Locally Available Techniques:\n") 193 | 194 | for _, desc := range descriptions { 195 | art.Println(desc) 196 | } 197 | } 198 | 199 | orgBranch := strings.Split(art.REPO, "/") 200 | 201 | if len(orgBranch) != 2 { 202 | return cli.Exit("repo must be in format /", 1) 203 | } 204 | 205 | url := fmt.Sprintf("https://github.com/%s/atomic-red-team/tree/%s/atomics", orgBranch[0], orgBranch[1]) 206 | 207 | art.Printf("Please see %s for a list of available default techniques", url) 208 | 209 | return nil 210 | } 211 | 212 | if name == "" && index == -1 { 213 | if dump := ctx.String("dump-technique"); dump != "" { 214 | dir, err := art.DumpTechnique(dump, tid) 215 | if err != nil { 216 | return cli.Exit("error dumping technique: "+err.Error(), 1) 217 | } 218 | 219 | art.Printf("technique %s files dumped to %s", tid, dir) 220 | 221 | return nil 222 | } 223 | 224 | technique, err := art.GetTechnique(tid) 225 | if err != nil { 226 | return cli.Exit("error getting details for "+tid, 1) 227 | } 228 | 229 | art.Printf("Technique: %s - %s\n", technique.AttackTechnique, technique.DisplayName) 230 | art.Println("Tests:") 231 | 232 | for i, t := range technique.AtomicTests { 233 | art.Printf(" %d. %s\n", i, t.Name) 234 | } 235 | 236 | md, err := art.GetMarkdown(tid) 237 | if err != nil { 238 | return cli.Exit("error getting Markdown for "+tid, 1) 239 | } 240 | 241 | if runtime.GOOS == "windows" { 242 | art.Println(string(md)) 243 | } else { 244 | options := []glamour.TermRendererOption{glamour.WithWordWrap(100)} 245 | 246 | if ctx.Bool("no-color") { 247 | options = append(options, glamour.WithColorProfile(termenv.Ascii)) 248 | } else { 249 | options = append(options, glamour.WithStylePath("dark")) 250 | } 251 | 252 | renderer, err := glamour.NewTermRenderer(options...) 253 | if err != nil { 254 | return cli.Exit("error creating new Markdown renderer", 1) 255 | } 256 | 257 | out, err := renderer.RenderBytes(md) 258 | if err != nil { 259 | return cli.Exit("error rendering Markdown for "+tid, 1) 260 | } 261 | 262 | art.Print(string(out)) 263 | } 264 | 265 | return nil 266 | } 267 | 268 | var err error 269 | 270 | art.TEMPDIR, err = os.MkdirTemp("", "goart-") 271 | if err != nil { 272 | return cli.Exit(err, 1) 273 | } 274 | 275 | defer os.RemoveAll(art.TEMPDIR) 276 | 277 | test, err := art.Execute(tid, name, index, inputs, env) 278 | if err != nil { 279 | return cli.Exit(err, 1) 280 | } 281 | 282 | var ( 283 | plan []byte 284 | ext = strings.ToLower(ctx.String("results-format")) 285 | ) 286 | 287 | switch ext { 288 | case "json": 289 | plan, _ = json.Marshal(test) 290 | case "yaml": 291 | plan, _ = yaml.Marshal(test) 292 | default: 293 | return cli.Exit("unknown results format provided", 1) 294 | } 295 | 296 | out := ctx.String("results-file") 297 | 298 | if out == "-" { 299 | art.Println() 300 | fmt.Println(string(plan)) 301 | return nil 302 | } 303 | 304 | if out == "" { 305 | now := strings.ReplaceAll(time.Now().UTC().Format(time.RFC3339), ":", ".") 306 | out = fmt.Sprintf("atomic-test-executor-execution-%s-%s.%s", tid, now, ext) 307 | } 308 | 309 | ioutil.WriteFile(out, plan, 0644) 310 | 311 | return nil 312 | }, 313 | } 314 | 315 | if art.REPO == "" { 316 | app.Flags = append( 317 | app.Flags, 318 | &cli.StringFlag{ 319 | Name: "repo", 320 | Aliases: []string{"r"}, 321 | Value: "redcanaryco/master", 322 | Usage: "Atomic Red Team repo/branch name", 323 | }, 324 | ) 325 | } 326 | 327 | if runtime.GOOS != "windows" { 328 | app.Flags = append( 329 | app.Flags, 330 | &cli.BoolFlag{ 331 | Name: "no-color", 332 | Usage: "disable printing colors to terminal", 333 | }, 334 | ) 335 | } 336 | 337 | if err := app.Run(os.Args); err != nil { 338 | fmt.Fprintf(os.Stderr, "%v\n", err) 339 | } 340 | 341 | art.Println() 342 | } 343 | -------------------------------------------------------------------------------- /executor.go: -------------------------------------------------------------------------------- 1 | package atomicredteam 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "net/http" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "runtime" 11 | "strings" 12 | 13 | "actshad.dev/go-atomicredteam/types" 14 | "gopkg.in/yaml.v3" 15 | ) 16 | 17 | func Execute(tid, name string, index int, inputs []string, env []string) (*types.AtomicTest, error) { 18 | test, err := getTest(tid, name, index) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | Println() 24 | 25 | Println("****** EXECUTION PLAN ******") 26 | Println(" Technique: " + tid) 27 | Println(" Test: " + test.Name) 28 | 29 | if inputs == nil { 30 | Println(" Inputs: ") 31 | } else { 32 | Println(" Inputs: " + strings.Join(inputs, "\n ")) 33 | } 34 | 35 | if env == nil { 36 | Println(" Env: ") 37 | } else { 38 | Println(" Env: " + strings.Join(env, "\n ")) 39 | } 40 | 41 | Println(" * Use at your own risk :) *") 42 | Println("****************************") 43 | 44 | args, err := checkArgsAndGetDefaults(test, inputs) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | if err := checkPlatform(test); err != nil { 50 | return nil, err 51 | } 52 | 53 | if len(test.Dependencies) != 0 { 54 | var found bool 55 | 56 | for _, e := range types.SupportedExecutors { 57 | if test.DependencyExecutorName == e { 58 | found = true 59 | break 60 | } 61 | } 62 | 63 | if !found { 64 | return nil, fmt.Errorf("dependency executor %s is not supported", test.DependencyExecutorName) 65 | } 66 | 67 | Printf("\nChecking dependencies...\n") 68 | 69 | for _, dep := range test.Dependencies { 70 | Printf(" - %s", dep.Description) 71 | 72 | command, err := interpolateWithArgs(dep.PrereqCommand, test.BaseDir, args, true) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | switch test.DependencyExecutorName { 78 | case "bash": 79 | _, err = executeBash(command, env) 80 | case "command_prompt": 81 | _, err = executeCommandPrompt(command, env) 82 | case "manual": 83 | _, err = executeManual(command) 84 | case "powershell": 85 | _, err = executePowerShell(command, env) 86 | case "sh": 87 | _, err = executeSh(command, env) 88 | } 89 | 90 | if err == nil { 91 | Printf(" * OK - dependency check succeeded!\n") 92 | continue 93 | } 94 | 95 | command, err = interpolateWithArgs(dep.GetPrereqCommand, test.BaseDir, args, true) 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | var result string 101 | 102 | switch test.DependencyExecutorName { 103 | case "bash": 104 | result, err = executeBash(command, env) 105 | case "command_prompt": 106 | result, err = executeCommandPrompt(command, env) 107 | case "manual": 108 | result, err = executeManual(command) 109 | case "powershell": 110 | result, err = executePowerShell(command, env) 111 | case "sh": 112 | result, err = executeSh(command, env) 113 | } 114 | 115 | if err != nil { 116 | if result == "" { 117 | result = "no details provided" 118 | } 119 | 120 | Printf(" * XX - dependency check failed: %s\n", result) 121 | 122 | return nil, fmt.Errorf("not all dependency checks passed") 123 | } 124 | } 125 | } 126 | 127 | if test.Executor == nil { 128 | return nil, fmt.Errorf("test has no executor") 129 | } 130 | 131 | var found bool 132 | 133 | for _, e := range types.SupportedExecutors { 134 | if test.Executor.Name == e { 135 | found = true 136 | break 137 | } 138 | } 139 | 140 | if !found { 141 | return nil, fmt.Errorf("executor %s is not supported", test.Executor.Name) 142 | } 143 | 144 | var interpolatee string 145 | 146 | if test.Executor.Name == "manual" { 147 | interpolatee = test.Executor.Steps 148 | } else { 149 | interpolatee = test.Executor.Command 150 | } 151 | 152 | command, err := interpolateWithArgs(interpolatee, test.BaseDir, args, false) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | Println("\nExecuting test...\n") 158 | 159 | var results string 160 | 161 | switch test.Executor.Name { 162 | case "bash": 163 | results, err = executeBash(command, env) 164 | case "command_prompt": 165 | results, err = executeCommandPrompt(command, env) 166 | case "manual": 167 | results, err = executeManual(command) 168 | case "powershell": 169 | results, err = executePowerShell(command, env) 170 | case "sh": 171 | results, err = executeSh(command, env) 172 | } 173 | 174 | if err != nil { 175 | if results != "" { 176 | Println("****** EXECUTOR FAILED ******") 177 | Println(results) 178 | Println("*****************************") 179 | } 180 | 181 | return nil, err 182 | } 183 | 184 | if test.Executor.Name != "manual" { 185 | Println("****** EXECUTOR RESULTS ******") 186 | Println(results) 187 | Println("******************************") 188 | } 189 | 190 | for k, v := range test.InputArugments { 191 | v.ExpectedValue = args[k] 192 | test.InputArugments[k] = v 193 | } 194 | 195 | test.Executor.ExecutedCommand = map[string]interface{}{ 196 | "command": command, 197 | "results": results, 198 | } 199 | 200 | return test, nil 201 | } 202 | 203 | func GetTechnique(tid string) (*types.Atomic, error) { 204 | if !strings.HasPrefix(tid, "T") { 205 | tid = "T" + tid 206 | } 207 | 208 | var body []byte 209 | 210 | if LOCAL != "" { 211 | // Check to see if test is defined locally first. If not, body will be nil 212 | // and the test will be loaded below. 213 | body, _ = os.ReadFile(LOCAL + "/" + tid + "/" + tid + ".yaml") 214 | if len(body) == 0 { 215 | body, _ = os.ReadFile(LOCAL + "/" + tid + "/" + tid + ".yml") 216 | } 217 | } 218 | 219 | if len(body) != 0 { 220 | var technique types.Atomic 221 | 222 | if err := yaml.Unmarshal(body, &technique); err != nil { 223 | return nil, fmt.Errorf("processing Atomic Test YAML file: %w", err) 224 | } 225 | 226 | technique.BaseDir = LOCAL 227 | return &technique, nil 228 | } 229 | 230 | if BUNDLED { 231 | body, base, err := Technique(tid) 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | var technique types.Atomic 237 | 238 | if err := yaml.Unmarshal(body, &technique); err != nil { 239 | return nil, fmt.Errorf("processing Atomic Test YAML file: %w", err) 240 | } 241 | 242 | technique.BaseDir = base 243 | return &technique, nil 244 | } 245 | 246 | orgBranch := strings.Split(REPO, "/") 247 | 248 | if len(orgBranch) != 2 { 249 | return nil, fmt.Errorf("repo must be in format / (name of repo in must be 'atomic-red-team')") 250 | } 251 | 252 | url := fmt.Sprintf("https://raw.githubusercontent.com/%s/atomic-red-team/%s/atomics/%s/%s.yaml", orgBranch[0], orgBranch[1], tid, tid) 253 | 254 | resp, err := http.Get(url) 255 | if err != nil { 256 | return nil, fmt.Errorf("getting Atomic Test from GitHub: %w", err) 257 | } 258 | 259 | defer resp.Body.Close() 260 | 261 | body, err = io.ReadAll(resp.Body) 262 | if err != nil { 263 | return nil, fmt.Errorf("reading Atomic Test from GitHub response: %w", err) 264 | } 265 | 266 | var technique types.Atomic 267 | 268 | if err := yaml.Unmarshal(body, &technique); err != nil { 269 | return nil, fmt.Errorf("processing Atomic Test YAML file: %w", err) 270 | } 271 | 272 | return &technique, nil 273 | } 274 | 275 | func GetMarkdown(tid string) ([]byte, error) { 276 | if !strings.HasPrefix(tid, "T") { 277 | tid = "T" + tid 278 | } 279 | 280 | var body []byte 281 | 282 | if LOCAL != "" { 283 | // Check to see if test is defined locally first. If not, body will be nil 284 | // and the test will be loaded below. 285 | body, _ = os.ReadFile(LOCAL + "/" + tid + "/" + tid + ".md") 286 | } 287 | 288 | if len(body) == 0 { 289 | if BUNDLED { 290 | var err error 291 | 292 | if body, err = Markdown(tid); err != nil { 293 | return nil, err 294 | } 295 | } else { 296 | orgBranch := strings.Split(REPO, "/") 297 | 298 | if len(orgBranch) != 2 { 299 | return nil, fmt.Errorf("repo must be in format /") 300 | } 301 | 302 | url := fmt.Sprintf("https://raw.githubusercontent.com/%s/atomic-red-team/%s/atomics/%s/%s.md", orgBranch[0], orgBranch[1], tid, tid) 303 | 304 | resp, err := http.Get(url) 305 | if err != nil { 306 | return nil, fmt.Errorf("getting Atomic Test from GitHub: %w", err) 307 | } 308 | 309 | defer resp.Body.Close() 310 | 311 | body, err = io.ReadAll(resp.Body) 312 | if err != nil { 313 | return nil, fmt.Errorf("reading Atomic Test from GitHub response: %w", err) 314 | } 315 | } 316 | } 317 | 318 | // All the Markdown files wrap the ATT&CK technique descriptions in 319 | //
blocks, but Glamour doesn't render that correctly, so let's 320 | // remove them here. 321 | body = BlockQuoteRegex.ReplaceAll(body, nil) 322 | 323 | return body, nil 324 | } 325 | 326 | func DumpTechnique(dir, tid string) (string, error) { 327 | if !strings.HasPrefix(tid, "T") { 328 | tid = "T" + tid 329 | } 330 | 331 | var ( 332 | testBody []byte 333 | mdBody []byte 334 | ) 335 | 336 | // We don't check for locally defined techniques here since it makes no sense 337 | // to dump them to file when they're already present locally. 338 | 339 | // TODO: also dump any additional files bundled with techniques. 340 | 341 | if BUNDLED { 342 | var err error 343 | 344 | if testBody, _, err = Technique(tid); err != nil { 345 | return "", err 346 | } 347 | 348 | if mdBody, err = Markdown(tid); err != nil { 349 | return "", err 350 | } 351 | } else { 352 | orgBranch := strings.Split(REPO, "/") 353 | 354 | if len(orgBranch) != 2 { 355 | return "", fmt.Errorf("repo must be in format /") 356 | } 357 | 358 | url := fmt.Sprintf("https://raw.githubusercontent.com/%s/atomic-red-team/%s/atomics/%s/%s.yaml", orgBranch[0], orgBranch[1], tid, tid) 359 | 360 | resp, err := http.Get(url) 361 | if err != nil { 362 | return "", fmt.Errorf("getting Atomic Test from GitHub: %w", err) 363 | } 364 | 365 | defer resp.Body.Close() 366 | 367 | testBody, err = io.ReadAll(resp.Body) 368 | if err != nil { 369 | return "", fmt.Errorf("reading Atomic Test from GitHub response: %w", err) 370 | } 371 | 372 | url = fmt.Sprintf("https://raw.githubusercontent.com/%s/atomic-red-team/%s/atomics/%s/%s.md", orgBranch[0], orgBranch[1], tid, tid) 373 | 374 | resp, err = http.Get(url) 375 | if err != nil { 376 | return "", fmt.Errorf("getting Atomic Test from GitHub: %w", err) 377 | } 378 | 379 | defer resp.Body.Close() 380 | 381 | mdBody, err = io.ReadAll(resp.Body) 382 | if err != nil { 383 | return "", fmt.Errorf("reading Atomic Test from GitHub response: %w", err) 384 | } 385 | } 386 | 387 | dir = dir + "/" + tid 388 | 389 | if err := os.MkdirAll(dir, 0755); err != nil { 390 | return "", fmt.Errorf("creating local technique directory %s: %w", dir, err) 391 | } 392 | 393 | path := dir + "/" + tid + ".yaml" 394 | if err := os.WriteFile(path, testBody, 0644); err != nil { 395 | return "", fmt.Errorf("writing test configs for technique %s to %s: %w", tid, path, err) 396 | } 397 | 398 | path = dir + "/" + tid + ".md" 399 | if err := os.WriteFile(path, mdBody, 0644); err != nil { 400 | return "", fmt.Errorf("writing test documentation for technique %s to %s: %w", tid, path, err) 401 | } 402 | 403 | return dir, nil 404 | } 405 | 406 | func getTest(tid, name string, index int) (*types.AtomicTest, error) { 407 | Printf("\nGetting Atomic Tests technique %s from GitHub repo %s\n", tid, REPO) 408 | 409 | technique, err := GetTechnique(tid) 410 | if err != nil { 411 | return nil, fmt.Errorf("getting Atomic Tests technique: %w", err) 412 | } 413 | 414 | Printf(" - technique has %d tests\n", len(technique.AtomicTests)) 415 | 416 | var test *types.AtomicTest 417 | 418 | if index >= 0 && index < len(technique.AtomicTests) { 419 | test = &technique.AtomicTests[index] 420 | } else { 421 | for _, t := range technique.AtomicTests { 422 | if t.Name == name { 423 | test = &t 424 | break 425 | } 426 | } 427 | } 428 | 429 | if test == nil { 430 | return nil, fmt.Errorf("could not find test %s/%s", tid, name) 431 | } 432 | 433 | test.BaseDir = technique.BaseDir 434 | 435 | Printf(" - found test named %s\n", test.Name) 436 | 437 | return test, nil 438 | } 439 | 440 | func checkArgsAndGetDefaults(test *types.AtomicTest, inputs []string) (map[string]string, error) { 441 | var ( 442 | keys []string 443 | args = make(map[string]string) 444 | updated = make(map[string]string) 445 | ) 446 | 447 | if len(test.InputArugments) == 0 { 448 | return updated, nil 449 | } 450 | 451 | for _, i := range inputs { 452 | kv := strings.Split(i, "=") 453 | 454 | if len(kv) == 2 { 455 | keys = append(keys, kv[0]) 456 | args[kv[0]] = kv[1] 457 | } 458 | } 459 | 460 | Println("\nChecking arguments...") 461 | 462 | if len(keys) > 0 { 463 | Println(" - supplied on command line: " + strings.Join(keys, ", ")) 464 | } 465 | 466 | for k, v := range test.InputArugments { 467 | Println(" - checking for argument " + k) 468 | 469 | val, ok := args[k] 470 | 471 | if ok { 472 | Println(" * OK - found argument in supplied args") 473 | } else { 474 | Println(" * XX - not found, trying default arg") 475 | 476 | val = v.Default 477 | 478 | if val == "" { 479 | return nil, fmt.Errorf("argument [%s] is required but not set and has no default", k) 480 | } else { 481 | Println(" * OK - found argument in defaults") 482 | } 483 | } 484 | 485 | updated[k] = val 486 | } 487 | 488 | return updated, nil 489 | } 490 | 491 | func checkPlatform(test *types.AtomicTest) error { 492 | var platform string 493 | 494 | switch runtime.GOOS { 495 | case "linux", "freebsd", "netbsd", "openbsd", "solaris": 496 | platform = "linux" 497 | case "darwin": 498 | platform = "macos" 499 | case "windows": 500 | platform = "windows" 501 | } 502 | 503 | if platform == "" { 504 | return fmt.Errorf("unable to detect our platform") 505 | } 506 | 507 | Printf("\nChecking platform vs our platform (%s)...\n", platform) 508 | 509 | var found bool 510 | 511 | for _, p := range test.SupportedPlatforms { 512 | if p == platform { 513 | found = true 514 | break 515 | } 516 | } 517 | 518 | if found { 519 | Println(" - OK - our platform is supported!") 520 | } else { 521 | return fmt.Errorf("unable to run test that supports platforms %v because we are on %s", test.SupportedPlatforms, platform) 522 | } 523 | 524 | return nil 525 | } 526 | 527 | func interpolateWithArgs(interpolatee, base string, args map[string]string, quiet bool) (string, error) { 528 | interpolated := strings.TrimSpace(interpolatee) 529 | 530 | if len(args) == 0 { 531 | return interpolated, nil 532 | } 533 | 534 | prevQuiet := Quiet 535 | Quiet = quiet 536 | 537 | defer func() { 538 | Quiet = prevQuiet 539 | }() 540 | 541 | Println("\nInterpolating command with input arguments...") 542 | 543 | for k, v := range args { 544 | Printf(" - interpolating [#{%s}] => [%s]\n", k, v) 545 | 546 | if AtomicsFolderRegex.MatchString(v) { 547 | v = AtomicsFolderRegex.ReplaceAllString(v, "") 548 | v = strings.ReplaceAll(v, `\`, `/`) 549 | v = strings.TrimSuffix(base, "/") + "/" + v 550 | 551 | // TODO: handle requesting file from GitHub repo if not bundled. 552 | if base != LOCAL { 553 | body, err := include.ReadFile(v) 554 | if err != nil { 555 | return "", fmt.Errorf("reading %s: %w", k, err) 556 | } 557 | 558 | v = filepath.FromSlash(TEMPDIR + "/" + v) 559 | 560 | if err := os.MkdirAll(filepath.Dir(v), 0700); err != nil { 561 | return "", fmt.Errorf("creating directory structure for %s: %w", k, err) 562 | } 563 | 564 | if err := os.WriteFile(v, body, 0644); err != nil { 565 | return "", fmt.Errorf("restoring %s: %w", k, err) 566 | } 567 | } 568 | } 569 | 570 | interpolated = strings.ReplaceAll(interpolated, "#{"+k+"}", v) 571 | } 572 | 573 | return interpolated, nil 574 | } 575 | 576 | func executeCommandPrompt(command string, env []string) (string, error) { 577 | // Printf("\nExecuting executor=cmd command=[%s]\n", command) 578 | 579 | f, err := os.Create(TEMPDIR + "/goart.bat") 580 | if err != nil { 581 | return "", fmt.Errorf("creating temporary file: %w", err) 582 | } 583 | 584 | if _, err := f.Write([]byte(command)); err != nil { 585 | f.Close() 586 | 587 | return "", fmt.Errorf("writing command to file: %w", err) 588 | } 589 | 590 | if err := f.Close(); err != nil { 591 | return "", fmt.Errorf("closing batch file: %w", err) 592 | } 593 | 594 | cmd := exec.Command("cmd.exe", "/c", f.Name()) 595 | cmd.Env = append(os.Environ(), env...) 596 | 597 | output, err := cmd.CombinedOutput() 598 | if err != nil { 599 | return string(output), fmt.Errorf("executing batch file: %w", err) 600 | } 601 | 602 | return string(output), nil 603 | } 604 | 605 | func executeSh(command string, env []string) (string, error) { 606 | // Printf("\nExecuting executor=sh command=[%s]\n", command) 607 | 608 | f, err := os.Create(TEMPDIR + "/goart.sh") 609 | if err != nil { 610 | return "", fmt.Errorf("creating temporary file: %w", err) 611 | } 612 | 613 | if _, err := f.Write([]byte(command)); err != nil { 614 | f.Close() 615 | 616 | return "", fmt.Errorf("writing command to file: %w", err) 617 | } 618 | 619 | if err := f.Close(); err != nil { 620 | return "", fmt.Errorf("closing shell script: %w", err) 621 | } 622 | 623 | cmd := exec.Command("sh", f.Name()) 624 | cmd.Env = append(os.Environ(), env...) 625 | 626 | output, err := cmd.CombinedOutput() 627 | if err != nil { 628 | return string(output), fmt.Errorf("executing shell script: %w", err) 629 | } 630 | 631 | return string(output), nil 632 | } 633 | 634 | func executeBash(command string, env []string) (string, error) { 635 | // Printf("\nExecuting executor=bash command=[%s]\n", command) 636 | 637 | f, err := os.Create(TEMPDIR + "/goart.bash") 638 | if err != nil { 639 | return "", fmt.Errorf("creating temporary file: %w", err) 640 | } 641 | 642 | if _, err := f.Write([]byte(command)); err != nil { 643 | f.Close() 644 | 645 | return "", fmt.Errorf("writing command to file: %w", err) 646 | } 647 | 648 | if err := f.Close(); err != nil { 649 | return "", fmt.Errorf("closing bash script: %w", err) 650 | } 651 | 652 | cmd := exec.Command("bash", f.Name()) 653 | cmd.Env = append(os.Environ(), env...) 654 | 655 | output, err := cmd.CombinedOutput() 656 | if err != nil { 657 | return string(output), fmt.Errorf("executing bash script: %w", err) 658 | } 659 | 660 | return string(output), nil 661 | } 662 | 663 | func executePowerShell(command string, env []string) (string, error) { 664 | // Printf("\nExecuting executor=powershell command=[%s]\n", command) 665 | 666 | f, err := os.Create(TEMPDIR + "/goart.ps1") 667 | if err != nil { 668 | return "", fmt.Errorf("creating temporary file: %w", err) 669 | } 670 | 671 | if _, err := f.Write([]byte(command)); err != nil { 672 | f.Close() 673 | 674 | return "", fmt.Errorf("writing command to file: %w", err) 675 | } 676 | 677 | if err := f.Close(); err != nil { 678 | return "", fmt.Errorf("closing PowerShell script: %w", err) 679 | } 680 | 681 | cmd := exec.Command("powershell", "-NoProfile", f.Name()) 682 | cmd.Env = append(os.Environ(), env...) 683 | 684 | output, err := cmd.CombinedOutput() 685 | if err != nil { 686 | return string(output), fmt.Errorf("executing PowerShell script: %w", err) 687 | } 688 | 689 | return string(output), nil 690 | } 691 | 692 | func executeManual(command string) (string, error) { 693 | // Println("\nExecuting executor=manual command=[]") 694 | 695 | steps := strings.Split(command, "\n") 696 | 697 | fmt.Printf("\nThe following steps should be executed manually:\n\n") 698 | 699 | for _, step := range steps { 700 | fmt.Printf(" %s\n", step) 701 | } 702 | 703 | return command, nil 704 | } 705 | --------------------------------------------------------------------------------