Skip to content

Commit

Permalink
feat(cmd): Add CLI Tool (#244)
Browse files Browse the repository at this point in the history
  • Loading branch information
jshlbrd authored Oct 3, 2024
1 parent f3f1476 commit 55a62bc
Show file tree
Hide file tree
Showing 8 changed files with 636 additions and 0 deletions.
136 changes: 136 additions & 0 deletions cmd/substation/demo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package main

import (
"context"
"encoding/json"
"fmt"

"github.com/brexhq/substation/v2"
"github.com/brexhq/substation/v2/message"
"github.com/google/go-jsonnet"
"github.com/spf13/cobra"
"github.com/tidwall/gjson"
)

func init() {
rootCmd.AddCommand(demoCmd)
}

const demoConf = `
local sub = import '../../substation.libsonnet';
{
transforms: [
// Move the event to the 'event.original' field.
sub.tf.obj.cp({object: { source_key: '@this', target_key: 'meta event.original' }}),
sub.tf.obj.cp({object: { source_key: 'meta @this' }}),
// Insert the hash of the original event into the 'event.hash' field.
sub.tf.hash.sha256({obj: { src: 'event.original', trg: 'event.hash'}}),
// Insert the event dataset into the 'event.dataset' field.
sub.tf.obj.insert({obj: { trg: 'event.dataset' }, value: 'aws.cloudtrail'}),
// Insert the kind of event into the 'event.kind' field.
sub.tf.obj.insert({obj: { trg: 'event.kind' }, value: 'event'}),
// Insert the event category into the 'event.category' field.
sub.tf.obj.insert({obj: { trg: std.format('%s.-1', 'event.category') }, value: 'configuration'}),
// Insert the event type into the 'event.type' field.
sub.tf.obj.insert({obj: { trg: std.format('%s.-1', 'event.type') }, value: 'change'}),
// Insert the outcome into the 'event.outcome' field.
sub.tf.meta.switch({ cases: [
{
condition: sub.cnd.num.len.gt({ obj: { src: 'errorCode' }, value: 0 }),
transforms: [
sub.tf.obj.insert({ obj: { trg: 'event.outcome' }, value: 'failure' }),
],
},
{
transforms: [
sub.tf.obj.insert({ obj: { trg: 'event.outcome' }, value: 'success' }),
],
},
] }),
// Copy the event time to the '@timestamp' field.
sub.tf.obj.cp({obj: { src: 'event.original.eventTime', trg: '\\@timestamp' }}),
// Copy the IP address to the 'source.ip' field.
sub.tf.obj.cp({obj: { src: 'event.original.sourceIPAddress', trg: 'source.ip' }}),
// Copy the user agent to the 'user_agent.original' field.
sub.tf.obj.cp({obj: { src: 'event.original.userAgent', trg: 'user_agent.original' }}),
// Copy the region to the 'cloud.region' field.
sub.tf.obj.cp({obj: { src: 'event.original.awsRegion', trg: 'cloud.region' }}),
// Copy the account ID to the 'cloud.account.id' field.
sub.tf.obj.cp({obj: { src: 'event.original.userIdentity.accountId', trg: 'cloud.account.id' }}),
// Add the cloud service provider to the 'cloud.provider' field.
sub.tf.obj.insert({obj: { trg: 'cloud.provider' }, value: 'aws'}),
// Extract the cloud service into the 'cloud.service.name' field.
sub.tf.str.capture({obj: { src: 'event.original.eventSource', trg: 'cloud.service.name' }, pattern: '^(.*)\\.amazonaws\\.com$'}),
// Make the event pretty before printing to the console.
sub.tf.obj.cp({obj: { src: '@this|@pretty' }}),
sub.tf.send.stdout(),
],
}
`

var demoCmd = &cobra.Command{
Use: "demo",
Short: "demo substation",
Long: `'substation demo' shows how Substation transforms data.
It prints an anonymized CloudTrail event (input) and the
transformed result (output) to the console. The event is
partially normalized to the Elastic Common Schema (ECS).
`,
// Examples:
// substation demo
Example: ` substation demo
`,
Args: cobra.MaximumNArgs(0),
RunE: func(cmd *cobra.Command, args []string) error {
cfg := substation.Config{}

vm := jsonnet.MakeVM()
res, err := vm.EvaluateAnonymousSnippet("demo", demoConf)
if err != nil {
return err
}

if err := json.Unmarshal([]byte(res), &cfg); err != nil {
return err
}

ctx := context.Background() // This doesn't need to be canceled.
sub, err := substation.New(ctx, cfg)
if err != nil {
return err
}

evt := `{"eventVersion":"1.08","userIdentity":{"type":"IAMUser","principalId":"EXAMPLE123456789","arn":"arn:aws:iam::123456789012:user/Alice","accountId":"123456789012","accessKeyId":"ASIAEXAMPLE123","sessionContext":{"attributes":{"mfaAuthenticated":"false","creationDate":"2024-10-01T12:00:00Z"},"sessionIssuer":{"type":"AWS","principalId":"EXAMPLE123456","arn":"arn:aws:iam::123456789012:role/Admin","accountId":"123456789012","userName":"Admin"}}},"eventTime":"2024-10-01T12:30:45Z","eventSource":"s3.amazonaws.com","eventName":"PutBucketPolicy","awsRegion":"us-west-2","sourceIPAddress":"203.0.113.0","userAgent":"aws-sdk-python/1.0.0 Python/3.8.0 Linux/4.15.0","requestParameters":{"bucketName":"example-bucket","policy":"{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":\"*\",\"Action\":\"s3:GetObject\",\"Resource\":\"arn:aws:s3:::example-bucket/*\"}]}"}},"responseElements":{"location":"http://example-bucket.s3.amazonaws.com/"},"requestID":"EXAMPLE123456789","eventID":"EXAMPLE-1-2-3-4-5-6","readOnly":false,"resources":[{"ARN":"arn:aws:s3:::example-bucket","accountId":"123456789012","type":"AWS::S3::Bucket"}],"eventType":"AwsApiCall","managementEvent":true,"recipientAccountId":"123456789012"}`
msgs := []*message.Message{
message.New().SetData([]byte(evt)),
message.New().AsControl(),
}

// Make the input pretty before printing to the console.
fmt.Printf("input:\n%s\n", gjson.Get(evt, "@this|@pretty").String())
fmt.Printf("output:\n")

if _, err := sub.Transform(ctx, msgs...); err != nil {
return err
}

fmt.Printf("\nconfig:%s\n", demoConf)

return nil
},
}
50 changes: 50 additions & 0 deletions cmd/substation/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"os"

"github.com/google/go-jsonnet"
"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
Use: "substation",
Long: "'substation' is a tool for managing Substation configurations.",
}

func init() {
// Hides the 'completion' command.
rootCmd.AddCommand(&cobra.Command{
Use: "completion",
Short: "generate the autocompletion script for the specified shell",
Hidden: true,
})

// Hides the 'help' command.
rootCmd.SetHelpCommand(&cobra.Command{
Use: "no-help",
Hidden: true,
})
}

// buildFile returns JSON from a Jsonnet file.
func buildFile(f string, extVars map[string]string) (string, error) {
vm := jsonnet.MakeVM()
for k, v := range extVars {
vm.ExtVar(k, v)
}

res, err := vm.EvaluateFile(f)
if err != nil {
return "", err
}

return res, nil
}

func main() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
Loading

0 comments on commit 55a62bc

Please sign in to comment.