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 }