Skip to content

Commit b65fbd3

Browse files
committed
Introduce Threatest CLI
1 parent d7daa5a commit b65fbd3

32 files changed

Lines changed: 1080 additions & 76 deletions

.github/workflows/docker-release.yml

Whitespace-only changes.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
.idea
2+
dist/

Dockerfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM golang:1.19-alpine3.16@sha256:4e2a54594cfe7002a98c483c28f6f3a78e5c7f4010c355a8cf960292a3fdecfe AS builder
2+
ARG VERSION=dev-snapshot
3+
RUN mkdir /build
4+
RUN apk add --update make
5+
WORKDIR /build
6+
ADD . /build
7+
RUN make BUILD_VERSION=${VERSION}
8+
9+
FROM alpine:3.16@sha256:3d426b0bfc361d6e8303f51459f17782b219dece42a1c7fe463b6014b189c86d AS runner
10+
LABEL org.opencontainers.image.source="https://github.com/DataDog/threatest/"
11+
COPY --from=builder /build/bin/threatest /threatest
12+
ENTRYPOINT ["/threatest"]
13+
CMD ["--help"]

LICENSE-3rdparty.csv

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,13 @@ github.com/aws/aws-sdk-go-v2/service/sso,https://github.com/aws/aws-sdk-go-v2/bl
3030
github.com/aws/aws-sdk-go-v2/service/sts,https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.14.0/service/sts/LICENSE.txt,Apache-2.0
3131
github.com/aws/smithy-go,https://github.com/aws/smithy-go/blob/v1.12.0/LICENSE,Apache-2.0
3232
github.com/datadog/stratus-red-team/v2,https://github.com/datadog/stratus-red-team/blob/v2.2.3/v2/LICENSE,Apache-2.0
33-
github.com/datadog/threatest/pkg/threatest,Unknown,Unknown
34-
github.com/datadog/threatest/pkg/threatest/detonators,Unknown,Unknown
35-
github.com/datadog/threatest/pkg/threatest/matchers,Unknown,Unknown
33+
github.com/datadog/threatest/pkg/threatest,https://github.com/datadog/threatest/blob/HEAD/LICENSE,Apache-2.0
3634
github.com/davecgh/go-spew/spew,https://github.com/davecgh/go-spew/blob/v1.1.1/LICENSE,ISC
3735
github.com/go-logr/logr,https://github.com/go-logr/logr/blob/v1.2.0/LICENSE,Apache-2.0
3836
github.com/gogo/protobuf,https://github.com/gogo/protobuf/blob/v1.3.2/LICENSE,BSD-3-Clause
3937
github.com/golang-jwt/jwt,https://github.com/golang-jwt/jwt/blob/v3.2.2/LICENSE,MIT
4038
github.com/golang/protobuf,https://github.com/golang/protobuf/blob/v1.5.2/LICENSE,BSD-3-Clause
41-
github.com/google/go-cmp/cmp,https://github.com/google/go-cmp/blob/v0.5.8/LICENSE,BSD-3-Clause
39+
github.com/google/go-cmp/cmp,https://github.com/google/go-cmp/blob/v0.5.9/LICENSE,BSD-3-Clause
4240
github.com/google/gofuzz,https://github.com/google/gofuzz/blob/v1.1.0/LICENSE,Apache-2.0
4341
github.com/google/uuid,https://github.com/google/uuid/blob/v1.3.0/LICENSE,BSD-3-Clause
4442
github.com/googleapis/gnostic,https://github.com/googleapis/gnostic/blob/v0.5.5/LICENSE,Apache-2.0
@@ -59,26 +57,26 @@ github.com/modern-go/reflect2,https://github.com/modern-go/reflect2/blob/v1.0.2/
5957
github.com/pkg/browser,https://github.com/pkg/browser/blob/ce105d075bb4/LICENSE,BSD-2-Clause
6058
github.com/spf13/pflag,https://github.com/spf13/pflag/blob/v1.0.5/LICENSE,BSD-3-Clause
6159
github.com/zclconf/go-cty/cty,https://github.com/zclconf/go-cty/blob/v1.9.1/LICENSE,MIT
62-
golang.org/x/crypto,https://cs.opensource.google/go/x/crypto/+/c6db032c:LICENSE,BSD-3-Clause
63-
golang.org/x/net,https://cs.opensource.google/go/x/net/+/2871e0cb:LICENSE,BSD-3-Clause
64-
golang.org/x/oauth2,https://cs.opensource.google/go/x/oauth2/+/d3ed0bb2:LICENSE,BSD-3-Clause
65-
golang.org/x/sys,https://cs.opensource.google/go/x/sys/+/5e4e11fc:LICENSE,BSD-3-Clause
66-
golang.org/x/term,https://cs.opensource.google/go/x/term/+/03fcf44c:LICENSE,BSD-3-Clause
67-
golang.org/x/text,https://cs.opensource.google/go/x/text/+/v0.3.7:LICENSE,BSD-3-Clause
68-
golang.org/x/time/rate,https://cs.opensource.google/go/x/time/+/1f47c861:LICENSE,BSD-3-Clause
69-
google.golang.org/protobuf,https://github.com/protocolbuffers/protobuf-go/blob/v1.27.1/LICENSE,BSD-3-Clause
60+
golang.org/x/crypto,https://cs.opensource.google/go/x/crypto/+/v0.1.0:LICENSE,BSD-3-Clause
61+
golang.org/x/net,https://cs.opensource.google/go/x/net/+/v0.1.0:LICENSE,BSD-3-Clause
62+
golang.org/x/oauth2,https://cs.opensource.google/go/x/oauth2/+/6fdb5e3d:LICENSE,BSD-3-Clause
63+
golang.org/x/sys/unix,https://cs.opensource.google/go/x/sys/+/v0.1.0:LICENSE,BSD-3-Clause
64+
golang.org/x/term,https://cs.opensource.google/go/x/term/+/v0.1.0:LICENSE,BSD-3-Clause
65+
golang.org/x/text,https://cs.opensource.google/go/x/text/+/v0.4.0:LICENSE,BSD-3-Clause
66+
golang.org/x/time/rate,https://cs.opensource.google/go/x/time/+/579cf78f:LICENSE,BSD-3-Clause
67+
google.golang.org/protobuf,https://github.com/protocolbuffers/protobuf-go/blob/v1.28.1/LICENSE,BSD-3-Clause
7068
gopkg.in/alessio/shellescape.v1,https://github.com/alessio/shellescape/blob/52074bc9df61/LICENSE,MIT
7169
gopkg.in/inf.v0,https://github.com/go-inf/inf/blob/v0.9.1/LICENSE,BSD-3-Clause
7270
gopkg.in/yaml.v2,https://github.com/go-yaml/yaml/blob/v2.4.0/LICENSE,Apache-2.0
73-
gopkg.in/yaml.v3,https://github.com/go-yaml/yaml/blob/496545a6307b/LICENSE,MIT
71+
gopkg.in/yaml.v3,https://github.com/go-yaml/yaml/blob/v3.0.1/LICENSE,MIT
7472
k8s.io/api,https://github.com/kubernetes/api/blob/v0.23.3/LICENSE,Apache-2.0
7573
k8s.io/apimachinery/pkg,https://github.com/kubernetes/apimachinery/blob/v0.23.3/LICENSE,Apache-2.0
7674
k8s.io/apimachinery/third_party/forked/golang,https://github.com/kubernetes/apimachinery/blob/v0.23.3/third_party/forked/golang/LICENSE,BSD-3-Clause
7775
k8s.io/client-go,https://github.com/kubernetes/client-go/blob/v0.23.3/LICENSE,Apache-2.0
78-
k8s.io/klog/v2,https://github.com/kubernetes/klog/blob/v2.30.0/LICENSE,Apache-2.0
76+
k8s.io/klog/v2,https://github.com/kubernetes/klog/blob/v2.80.1/LICENSE,Apache-2.0
7977
k8s.io/kube-openapi/pkg,https://github.com/kubernetes/kube-openapi/blob/e816edb12b65/LICENSE,Apache-2.0
8078
k8s.io/utils,https://github.com/kubernetes/utils/blob/6203023598ed/LICENSE,Apache-2.0
8179
k8s.io/utils/internal/third_party/forked/golang/net,https://github.com/kubernetes/utils/blob/6203023598ed/internal/third_party/forked/golang/LICENSE,BSD-3-Clause
8280
sigs.k8s.io/json,https://github.com/kubernetes-sigs/json/blob/c049b76a60c6/LICENSE,Apache-2.0
8381
sigs.k8s.io/structured-merge-diff/v4,https://github.com/kubernetes-sigs/structured-merge-diff/blob/v4.2.1/LICENSE,Apache-2.0
84-
sigs.k8s.io/yaml,https://github.com/kubernetes-sigs/yaml/blob/v1.2.0/LICENSE,MIT
82+
sigs.k8s.io/yaml,https://github.com/kubernetes-sigs/yaml/blob/v1.3.0/LICENSE,MIT

Makefile

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
1-
.PHONY: mocks
1+
.PHONY: mocks parser
22

33
MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
44
ROOT_DIR := $(dir $(MAKEFILE_PATH))
55

6+
all: build
7+
8+
build:
9+
mkdir -p bin
10+
go build -o bin/threatest cmd/threatest/*.go
11+
612
test:
713
go test $(shell go list ./... | grep -v examples)
814

915
thirdparty-licenses:
1016
go get github.com/google/go-licenses
1117
go install github.com/google/go-licenses
12-
$(GOPATH)/bin/go-licenses csv github.com/datadog/threatest/pkg/threatest | sort > $(ROOT_DIR)/LICENSE-3rdparty.csv
18+
$${GOPATH}/bin/go-licenses csv github.com/datadog/threatest/pkg/threatest | sort > $(ROOT_DIR)/LICENSE-3rdparty.csv
1319

1420
mocks:
1521
mockery --name=Detonator --dir pkg/threatest/detonators/ --output pkg/threatest/detonators/mocks
1622
mockery --name=AlertGeneratedMatcher --dir pkg/threatest/matchers/ --output pkg/threatest/matchers/mocks
1723
mockery --name=DatadogSecuritySignalsAPI --dir pkg/threatest/matchers/datadog --output pkg/threatest/matchers/datadog/mocks
24+
25+
parser:
26+
go get github.com/atombender/go-jsonschema/...
27+
go install github.com/atombender/go-jsonschema/cmd/gojsonschema@latest
28+
$${GOPATH}/bin/gojsonschema -p parser schemas/threatest.schema.json > pkg/threatest/parser/parser.go

README.md

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,95 @@ Each detonation is assigned a UUID. This UUID is reflected in the detonation and
3434

3535
The way this is done depends on the detonator; for instance, Stratus Red Team and the AWS Detonator inject it in the user-agent; the SSH detonator uses a parent process containing the UUID.
3636

37-
## Sample usage
37+
## Usage
3838

39-
See [examples](./examples) for complete usage example.
39+
### Through the CLI
4040

41-
### Testing Datadog Cloud SIEM signals triggered by Stratus Red Team
41+
Threatest comes with a CLI that you can use to run test scenarios described as YAML, following a specific [schema](./schemas/threatest.schema.json). You can configure this schema in your editor to benefit from in-IDE linting and autocompletion (see [documentation for VSCode](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml#associating-a-schema-to-a-glob-pattern-via-yaml.schemas) using the [YAML](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) extension).
42+
43+
Sample usage:
44+
45+
```bash
46+
$ threatest lint scenarios.threatest.yaml
47+
All 6 scenarios are syntaxically valid
48+
49+
# Local detonation
50+
$ threatest run local-scenarios.threatest.yaml
51+
52+
# Remote detonation over SSH
53+
$ threatest run scenarios.threatest.yaml --ssh-host test-box --ssh-username vagrant
54+
55+
# Alternatively, specify SSH parameters from environment variables
56+
$ export THREATEST_SSH_HOST=test-box
57+
$ export THREATEST_SSH_USERNAME=vagrant
58+
$ threatest run scenarios.threatest.yaml
59+
```
60+
61+
Sample scenario definition file:
62+
63+
```yaml
64+
scenarios:
65+
- description: curl metadata service
66+
detonate:
67+
remoteDetonator:
68+
commands: ["curl http://169.254.169.254 --connect-timeout 1"]
69+
expectations:
70+
- timeout: 1m
71+
datadogSecuritySignal:
72+
name: "Network utility accessed cloud metadata service"
73+
severity: medium
74+
75+
- description: running nmap
76+
detonate:
77+
remoteDetonator:
78+
commands:
79+
- "which nmap || sudo apt install -y nmap"
80+
- "nmap -sn 172.16.2.1/32 -T5"
81+
expectations:
82+
- timeout: 1m
83+
datadogSecuritySignal:
84+
name: Network scanning utility executed
85+
```
86+
87+
88+
You can output the test results to a JSON file:
89+
90+
```
91+
$ threatest run scenarios.threatest.yaml --output test-results.json
92+
$ cat test-results.json
93+
[
94+
{
95+
"description": "change user password",
96+
"isSuccess": true,
97+
"errorMessage": "",
98+
"durationSeconds": 22.046627348,
99+
"timeDetonated": "2022-11-15T22:26:14.182844+01:00"
100+
},
101+
{
102+
"description": "adding an SSH key",
103+
"isSuccess": true,
104+
"errorMessage": "",
105+
"durationSeconds": 23.604699625,
106+
"timeDetonated": "2022-11-15T22:26:14.182832+01:00"
107+
},
108+
{
109+
"description": "change user password",
110+
"isSuccess": false,
111+
"errorMessage": "At least one scenario failed:\n\nchange user password returned: change user password: 1 assertions did not pass\n =\u003e Did not find Datadog security signal 'bar'\n",
112+
"durationSeconds": 3.505294235,
113+
"timeDetonated": "2022-11-15T22:26:36.229349+01:00"
114+
}
115+
]
116+
```
117+
118+
By default, scenarios are run with a maximum parallelism of 5. You can increase this setting using the `--parallelism` argument.
119+
Note that when using remote SSH detonators, each scenario running establishes a new SSH connection.
120+
121+
### Using Threatest programmatically
122+
123+
See [examples](./examples) for complete programmatic usage example.
124+
125+
#### Testing Datadog Cloud SIEM signals triggered by Stratus Red Team
42126

43127
```go
44128
threatest := Threatest()

cmd/threatest/lint.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"github.com/datadog/threatest/pkg/threatest"
7+
"github.com/datadog/threatest/pkg/threatest/parser"
8+
log "github.com/sirupsen/logrus"
9+
"github.com/spf13/cobra"
10+
"os"
11+
)
12+
13+
// LintCommand implements syntax verification of a Threatest scenario file
14+
type LintCommand struct {
15+
InputFiles []string
16+
}
17+
18+
func (m *LintCommand) Do() error {
19+
if len(m.InputFiles) == 0 {
20+
return errors.New("please provide at least 1 scenario")
21+
}
22+
var numScenarios = 0
23+
for _, inputFile := range m.InputFiles {
24+
rawScenario, err := os.ReadFile(inputFile)
25+
if err != nil {
26+
return fmt.Errorf("unable to read input file %s: %v", inputFile, err)
27+
}
28+
scenarios, err := parser.Parse(rawScenario, "unused", "", "")
29+
if err != nil {
30+
return fmt.Errorf("unable to parse input file %s: %v", inputFile, err)
31+
}
32+
for _, scenario := range scenarios {
33+
if err := validateScenario(scenario); err != nil {
34+
return fmt.Errorf("invalid scenario '%s': %s", scenario.Name, err.Error())
35+
}
36+
}
37+
numScenarios += len(scenarios)
38+
}
39+
log.Infof("All %d scenarios are syntaxically valid", numScenarios)
40+
return nil
41+
}
42+
43+
func validateScenario(scenario *threatest.Scenario) error {
44+
if scenario.Detonator == nil {
45+
return errors.New("no detonator defined")
46+
}
47+
if len(scenario.Assertions) == 0 {
48+
return errors.New("no assertion defined")
49+
}
50+
return nil
51+
}
52+
53+
func NewLintCommand() *cobra.Command {
54+
lintCmd := &cobra.Command{
55+
Use: "lint",
56+
Short: "Validate the format of scenarios",
57+
SilenceUsage: true,
58+
Example: "lint /path/to/scenario/1 [/path/to/scenario/2]...",
59+
RunE: func(cmd *cobra.Command, args []string) error {
60+
command := LintCommand{
61+
InputFiles: args,
62+
}
63+
return command.Do()
64+
},
65+
}
66+
67+
return lintCmd
68+
}

cmd/threatest/main.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"os"
6+
)
7+
8+
var rootCmd = &cobra.Command{
9+
Use: "threatest",
10+
}
11+
12+
func init() {
13+
rootCmd.AddCommand(NewRunCommand())
14+
rootCmd.AddCommand(NewLintCommand())
15+
}
16+
17+
func main() {
18+
if err := rootCmd.Execute(); err != nil {
19+
os.Exit(1)
20+
}
21+
}

0 commit comments

Comments
 (0)