├── 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 |

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 | [](https://pkg.go.dev/github.com/activeshadow/go-atomicredteam) [](https://goreportcard.com/report/github.com/activeshadow/go-atomicredteam)
15 |
16 |
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 |
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 | 
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 |
--------------------------------------------------------------------------------