github.com/facebookincubator/ttpforge@v1.0.13-0.20240405153150-5ae801628835/pkg/blocks/ttps.go (about)

     1  /*
     2  Copyright © 2023-present, Meta Platforms, Inc. and affiliates
     3  Permission is hereby granted, free of charge, to any person obtaining a copy
     4  of this software and associated documentation files (the "Software"), to deal
     5  in the Software without restriction, including without limitation the rights
     6  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  copies of the Software, and to permit persons to whom the Software is
     8  furnished to do so, subject to the following conditions:
     9  The above copyright notice and this permission notice shall be included in
    10  all copies or substantial portions of the Software.
    11  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    12  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    13  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    14  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    15  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    16  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    17  THE SOFTWARE.
    18  */
    19  
    20  package blocks
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"os"
    26  	"runtime"
    27  	"time"
    28  
    29  	"github.com/facebookincubator/ttpforge/pkg/checks"
    30  	"github.com/facebookincubator/ttpforge/pkg/logging"
    31  	"github.com/facebookincubator/ttpforge/pkg/platforms"
    32  	"gopkg.in/yaml.v3"
    33  )
    34  
    35  // TTP represents the top-level structure for a TTP
    36  // (Tactics, Techniques, and Procedures) object.
    37  //
    38  // **Attributes:**
    39  //
    40  // Environment: A map of environment variables to be set for the TTP.
    41  // Steps: An slice of steps to be executed for the TTP.
    42  // WorkDir: The working directory for the TTP.
    43  type TTP struct {
    44  	PreambleFields `yaml:",inline"`
    45  	Environment    map[string]string `yaml:"env,flow,omitempty"`
    46  	Steps          []Step            `yaml:"steps,omitempty,flow"`
    47  	// Omit WorkDir, but expose for testing.
    48  	WorkDir string `yaml:"-"`
    49  }
    50  
    51  // MitreAttack represents mappings to the MITRE ATT&CK framework.
    52  //
    53  // **Attributes:**
    54  //
    55  // Tactics: A string slice containing the MITRE ATT&CK tactic(s) associated with the TTP.
    56  // Techniques: A string slice containing the MITRE ATT&CK technique(s) associated with the TTP.
    57  // SubTechniques: A string slice containing the MITRE ATT&CK sub-technique(s) associated with the TTP.
    58  type MitreAttack struct {
    59  	Tactics       []string `yaml:"tactics,omitempty"`
    60  	Techniques    []string `yaml:"techniques,omitempty"`
    61  	SubTechniques []string `yaml:"subtechniques,omitempty"`
    62  }
    63  
    64  // MarshalYAML is a custom marshalling implementation for the TTP structure.
    65  // It encodes a TTP object into a formatted YAML string, handling the
    66  // indentation and structure of the output YAML.
    67  func (t *TTP) MarshalYAML() (interface{}, error) {
    68  	marshaled, err := yaml.Marshal(*t)
    69  	if err != nil {
    70  		return nil, fmt.Errorf("failed to marshal TTP to YAML: %v", err)
    71  	}
    72  
    73  	// This section is necessary to get the proper formatting.
    74  	// Resource: https://pkg.go.dev/gopkg.in/yaml.v3#section-readme
    75  	m := make(map[interface{}]interface{})
    76  
    77  	err = yaml.Unmarshal(marshaled, &m)
    78  	if err != nil {
    79  		return nil, fmt.Errorf("failed to unmarshal YAML: %v", err)
    80  	}
    81  
    82  	b, err := yaml.Marshal(m)
    83  	if err != nil {
    84  		return nil, fmt.Errorf("failed to marshal back to YAML: %v", err)
    85  	}
    86  
    87  	formattedYAML := reduceIndentation(b, 2)
    88  
    89  	return fmt.Sprintf("---\n%s", string(formattedYAML)), nil
    90  }
    91  
    92  func reduceIndentation(b []byte, n int) []byte {
    93  	lines := bytes.Split(b, []byte("\n"))
    94  
    95  	for i, line := range lines {
    96  		// Replace tabs with spaces for consistent processing
    97  		line = bytes.ReplaceAll(line, []byte("\t"), []byte("    "))
    98  
    99  		trimmedLine := bytes.TrimLeft(line, " ")
   100  		indentation := len(line) - len(trimmedLine)
   101  		if indentation >= n {
   102  			lines[i] = bytes.TrimPrefix(line, bytes.Repeat([]byte(" "), n))
   103  		} else {
   104  			lines[i] = trimmedLine
   105  		}
   106  	}
   107  
   108  	return bytes.Join(lines, []byte("\n"))
   109  }
   110  
   111  // Validate ensures that all components of the TTP are valid
   112  // It checks key fields, then iterates through each step
   113  // and validates them in turn
   114  func (t *TTP) Validate(execCtx TTPExecutionContext) error {
   115  	logging.L().Debugf("Validating TTP %q...", t.Name)
   116  
   117  	// Validate preamble fields
   118  	err := t.PreambleFields.Validate(false)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	// Validate steps
   124  	for _, step := range t.Steps {
   125  		stepCopy := step
   126  		if err := stepCopy.Validate(execCtx); err != nil {
   127  			return err
   128  		}
   129  	}
   130  	logging.L().Debug("...finished validating TTP.")
   131  	return nil
   132  }
   133  
   134  // Execute executes all of the steps in the given TTP,
   135  // then runs cleanup if appropriate
   136  func (t *TTP) Execute(execCtx TTPExecutionContext) error {
   137  	logging.L().Infof("RUNNING TTP: %v", t.Name)
   138  
   139  	if err := t.verifyPlatform(); err != nil {
   140  		return fmt.Errorf("TTP requirements not met: %w", err)
   141  	}
   142  
   143  	err := t.RunSteps(execCtx)
   144  	if err == nil {
   145  		logging.L().Info("All TTP steps completed successfully! ✅")
   146  	}
   147  	return err
   148  }
   149  
   150  // RunSteps executes all of the steps in the given TTP.
   151  func (t *TTP) RunSteps(execCtx TTPExecutionContext) error {
   152  	// go to the configuration directory for this TTP
   153  	changeBack, err := t.chdir()
   154  	if err != nil {
   155  		return err
   156  	}
   157  	defer changeBack()
   158  
   159  	var stepError error
   160  	var verifyError error
   161  	var shutdownFlag bool
   162  
   163  	// actually run all the steps
   164  	for stepIdx, step := range t.Steps {
   165  		logging.DividerThin()
   166  		logging.L().Infof("Executing Step #%d: %q", stepIdx+1, step.Name)
   167  		// core execution - run the step action
   168  		go func(step Step) {
   169  			_, err := step.Execute(execCtx)
   170  			if err != nil {
   171  				// This error was logged by the step itself
   172  				logging.L().Debugf("Error executing step %s: %v", step.Name, err)
   173  			}
   174  		}(step)
   175  
   176  		// await one of three outcomes:
   177  		// 1. step execution successful
   178  		// 2. step execution failed
   179  		// 3. shutdown signal received
   180  		select {
   181  		case stepResult := <-execCtx.actionResultsChan:
   182  			// step execution successful - record results
   183  			execResult := &ExecutionResult{
   184  				ActResult: *stepResult,
   185  			}
   186  			execCtx.StepResults.ByName[step.Name] = execResult
   187  			execCtx.StepResults.ByIndex = append(execCtx.StepResults.ByIndex, execResult)
   188  
   189  		case stepError = <-execCtx.errorsChan:
   190  			// this part is tricky - SubTTP steps
   191  			// must be cleaned up even on failure
   192  			// (because substeps may have succeeded)
   193  			// so in those cases, we need to save the result
   194  			// even if nil
   195  			if step.ShouldCleanupOnFailure() {
   196  				logging.L().Infof("[+] Cleaning up failed step %s", step.Name)
   197  				logging.L().Infof("[+] Full Cleanup will Run Afterward")
   198  				_, cleanupErr := step.Cleanup(execCtx)
   199  				if cleanupErr != nil {
   200  					logging.L().Errorf("Error cleaning up failed step %v: %v", step.Name, cleanupErr)
   201  				}
   202  			}
   203  
   204  		case shutdownFlag = <-execCtx.shutdownChan:
   205  			// TODO[nesusvet]: We should propagate signal to child processes if any
   206  			logging.L().Warn("Shutting down due to signal received")
   207  		}
   208  
   209  		// if the user specified custom success checks, run them now
   210  		verifyError = step.VerifyChecks()
   211  
   212  		if stepError != nil || verifyError != nil || shutdownFlag {
   213  			logging.L().Debug("[*] Stopping TTP Early")
   214  			break
   215  		}
   216  	}
   217  
   218  	logging.DividerThin()
   219  	if stepError != nil {
   220  		logging.L().Errorf("[*] Error executing TTP: %v", stepError)
   221  		return stepError
   222  	}
   223  	if verifyError != nil {
   224  		logging.L().Errorf("[*] Error verifying TTP: %v", verifyError)
   225  		return verifyError
   226  	}
   227  	if shutdownFlag {
   228  		return fmt.Errorf("[*] Shutting Down now")
   229  	}
   230  
   231  	return nil
   232  }
   233  
   234  // RunCleanup executes all required cleanup for steps in the given TTP.
   235  func (t *TTP) RunCleanup(execCtx TTPExecutionContext) error {
   236  	if execCtx.Cfg.NoCleanup {
   237  		logging.L().Info("[*] Skipping Cleanup as requested by Config")
   238  		return nil
   239  	}
   240  
   241  	if execCtx.Cfg.CleanupDelaySeconds > 0 {
   242  		logging.L().Infof("[*] Sleeping for Requested Cleanup Delay of %v Seconds", execCtx.Cfg.CleanupDelaySeconds)
   243  		time.Sleep(time.Duration(execCtx.Cfg.CleanupDelaySeconds) * time.Second)
   244  	}
   245  
   246  	// TODO[nesusvet]: We also should catch signals in clean ups
   247  	cleanupResults, err := t.startCleanupForCompletedSteps(execCtx)
   248  	if err != nil {
   249  		return err
   250  	}
   251  	// since ByIndex and ByName both contain pointers to
   252  	// the same underlying struct, this will update both
   253  	for cleanupIdx, cleanupResult := range cleanupResults {
   254  		execCtx.StepResults.ByIndex[cleanupIdx].Cleanup = cleanupResult
   255  	}
   256  
   257  	return nil
   258  }
   259  
   260  func (t *TTP) chdir() (func(), error) {
   261  	// note: t.WorkDir may not be set in tests but should
   262  	// be set when actually using `ttpforge run`
   263  	if t.WorkDir == "" {
   264  		logging.L().Info("Not changing working directory in tests")
   265  		return func() {}, nil
   266  	}
   267  	origDir, err := os.Getwd()
   268  	if err != nil {
   269  		return nil, err
   270  	}
   271  	if err := os.Chdir(t.WorkDir); err != nil {
   272  		return nil, err
   273  	}
   274  	return func() {
   275  		if err := os.Chdir(origDir); err != nil {
   276  			logging.L().Errorf("could not restore original directory %v: %v", origDir, err)
   277  		}
   278  	}, nil
   279  }
   280  
   281  // verify that we actually meet the necessary requirements to execute this TTP
   282  func (t *TTP) verifyPlatform() error {
   283  	verificationCtx := checks.VerificationContext{
   284  		Platform: platforms.Spec{
   285  			OS:   runtime.GOOS,
   286  			Arch: runtime.GOARCH,
   287  		},
   288  	}
   289  	return t.Requirements.Verify(verificationCtx)
   290  }
   291  
   292  func (t *TTP) startCleanupForCompletedSteps(execCtx TTPExecutionContext) ([]*ActResult, error) {
   293  	// go to the configuration directory for this TTP
   294  	changeBack, err := t.chdir()
   295  	if err != nil {
   296  		return nil, err
   297  	}
   298  	defer changeBack()
   299  
   300  	logging.DividerThick()
   301  	n := len(execCtx.StepResults.ByIndex)
   302  	logging.L().Infof("CLEANING UP %v steps of TTP: %q", n, t.Name)
   303  	cleanupResults := make([]*ActResult, n)
   304  	for cleanupIdx := n - 1; cleanupIdx >= 0; cleanupIdx-- {
   305  		stepToCleanup := t.Steps[cleanupIdx]
   306  		logging.DividerThin()
   307  		logging.L().Infof("Cleaning Up Step #%d: %q", cleanupIdx+1, stepToCleanup.Name)
   308  		cleanupResult, err := stepToCleanup.Cleanup(execCtx)
   309  		// must be careful to put these in step order, not in execution (reverse) order
   310  		cleanupResults[cleanupIdx] = cleanupResult
   311  		if err != nil {
   312  			logging.L().Errorf("error cleaning up step: %v", err)
   313  			logging.L().Errorf("will continue to try to cleanup other steps")
   314  			continue
   315  		}
   316  	}
   317  	logging.DividerThin()
   318  	logging.L().Info("Finished Cleanup Successfully ✅")
   319  	return cleanupResults, nil
   320  }