-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Templated Bash TTPs #61
Conversation
- Working on adding the ability to create a new ttp with templates - Closes #55
- Working bash template for basic and file-based TTPs - All new exported functions are commented and have unit tests
- Created integration tests for new.go - Closes #20 - Fixed incorrect path in pre-existing executeYAML_test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blocking on the Chdir beahvior as I think that's an important design issue to iron out - strongly encourage reconsidering that approach so that we can support brew install ttpforge
, internal TTP repos (for both us and clients), etc - happy to hear your thoughts
- Flagged by Sam here: #61 (comment)
- ExecuteYAML and test now live in the files package as per Sam's recommendations and feedback - Duplicate functionality in `run.go` - appears to be resolved in trunk already - Results from age of branch and when it was created - Added new functions to the files package as per Sam's feedback here: #61 (comment)
- Inlined test files employed for TestExecuteYAML - Removed old directories that appeared to be leftover from a previous PR - Updated yaml_test to table testing format as per https://github.com/facebookincubator/TTPForge/blob/main/docs/testing.md
The concerns outlined by you all should be addressed now. I just need to fix a few of the tests and we should be good. |
27f3a06
to
4ca6a07
Compare
- Decoupled bash reliance for template creation - Tests updated and working - Add ExpandHomeDir() and test - Updated TestPathExistsInInventory to be more realistic - Moved bash templates to their own directory - Fixed pkg tests to specifications in PR - Updated TemplateExists to support multiple inventoryPaths
- Updated codespell to not check go test files
Alright, everything is passing and ready for another review from you all. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, the tests still don't actually verify that the template code works as expected (see specific comment at appropriate point in the code) - you can still make arbitrary breaking changes and the tests will pass. I think you have three options:
- make the test assert against the template file contents, rather than trying to run it
- Call a lower level function (like ExecuteYAML) to run the generated template and check it's error status
- Remove the challenging template tests - make a TODO to add tests that can catch template regressions, land your PR and add them later
Any of these work for me, I can stamp once you implement one of them
To prevent this type of defect from sneaking through in the future, I recommend the TDD approach (in spirit if not literal practice) of deliberately introducing breaking changes to see if your tests break, then digging in and debugging if they don't break as expected.
cmd/new_test.go
Outdated
runOutput := new(bytes.Buffer) | ||
runCmd.SetOut(runOutput) | ||
|
||
err = runCmd.Execute() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still doesn't error out on an invalid template, so it won't catch regressions as you intent to with this test - I can modify templates/bash/bashTTP.yaml.tmpl
to have the contents THIS SHOULD CAUSE A FAILURE BUT DOES NOT
and the test will still pass - it's using the correct template after your latest change, but runCmd.Execute()
does not return an error because that cobra command is just implemented with a Run()
function that can't return an error.
I'm not sure there's an easy way around that - you could try changing Run() command to use log.Fatal but I think that will just straight-up kill your test before you hit any asserts, which will be very confusing for users. Putting more discussions in review summary.
Test result for reference (the ERROR
entries come from logging but don't actually cause the test to fail)
=== RUN TestCreateAndRunTTP
OMG TEST DIR: /var/folders/5w/hrcjpf993snbdj3236k6wvt80000gn/T/cmd-new-test2968834320
=== RUN TestCreateAndRunTTP/Create_basic_bash_TTP
2023-05-02T12:32:42.150-0500 INFO cmd/root.go:160 Using config file: config.yaml
2023-05-02T12:32:42.154-0500 INFO cmd/root.go:160 Using config file: config.yaml
2023-05-02T12:32:42.154-0500 ERROR files/yaml.go:38 failed to run TTP {"error": "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `THIS SH...` into blocks.TTPTmpl"}
2023-05-02T12:32:42.154-0500 ERROR cmd/run.go:42 failed to execute TTP {"error": "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `THIS SH...` into blocks.TTPTmpl"}
=== RUN TestCreateAndRunTTP/Create_file-based_bash_TTP
2023-05-02T12:32:42.154-0500 INFO cmd/root.go:160 Using config file: config.yaml
2023-05-02T12:32:42.156-0500 INFO cmd/root.go:160 Using config file: config.yaml
2023-05-02T12:32:42.157-0500 ERROR files/yaml.go:38 failed to run TTP {"error": "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `THIS SH...` into blocks.TTPTmpl"}
2023-05-02T12:32:42.157-0500 ERROR cmd/run.go:42 failed to execute TTP {"error": "yaml: unmarshal errors:\n line 1: cannot unmarshal !!str `THIS SH...` into blocks.TTPTmpl"}
--- PASS: TestCreateAndRunTTP (0.01s)
--- PASS: TestCreateAndRunTTP/Create_basic_bash_TTP (0.01s)
--- PASS: TestCreateAndRunTTP/Create_file-based_bash_TTP (0.00s)
PASS
ok github.com/facebookincubator/ttpforge/cmd 1.020s
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this runCmd.Execute will never error because there is no error return on RunTTPCmd. You will need to change the function of RunTTPCmd to return an error otherwise there is no way to flag this for success/failure
// if err != nil { | ||
// log.Fatalf("failed to create directory: %v", err) | ||
// } | ||
func CreateDirIfNotExists(fsys afero.Fs, path string) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused, remove in future
|
||
// This section is necessary to get the proper formatting. | ||
// Resource: https://pkg.go.dev/gopkg.in/yaml.v3#section-readme | ||
m := make(map[interface{}]interface{}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The usage of interface in both areas removes the ability to validate when writing back to disk. In the Marshal section of the readme you can use the defined struct in order to Marshal back out https://pkg.go.dev/gopkg.in/yaml.v3#Marshal
- Update TestCreateAndValidateTTP to check TTPForge's response to invalid TTPs
…ces aren't present
It won't run correctly: ~/pt/tools/ttpforge (i20*) » ./ttpforge -c config.yaml run ttps/lateral-movement/ssh/rogue-ssh-key.yaml jaysong@jaysong-mp
but the tests are passing, which is not ideal
We are using real template files for the tests - see this line in if err := copy. Copy(templatesDir, filepath.Join(testDir, "templates")); err != nil {
t.Fatalf("failed to copy templates dir: %v", err)
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests work now, other details present can be resolved at a later date.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
approved - left some pointers for the future, no need to change anything but if u could add a reply comment on whether or not the marshal stuff is dead code that'd be helpful
ship it
// interface{}: The formatted YAML string representing the TTP object. | ||
// | ||
// error: An error if the encoding process fails. | ||
func (t *TTP) MarshalYAML() (interface{}, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this code leftover from an earlier implementation? I was checking if the tests fail when I screw it up (to verify that issues noted earlier are addressed) and found that when I deleted the whole thing all tests still pass - the diff of my change:
-// MarshalYAML is a custom marshalling implementation for the TTP structure. It encodes a TTP object into a formatted
-// YAML string, handling the indentation and structure of the output YAML.
-//
-// Returns:
-//
-// interface{}: The formatted YAML string representing the TTP object.
-//
-// error: An error if the encoding process fails.
-func (t *TTP) MarshalYAML() (interface{}, error) {
- marshaled, err := yaml.Marshal(*t)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal TTP to YAML: %v", err)
- }
-
- // This section is necessary to get the proper formatting.
- // Resource: https://pkg.go.dev/gopkg.in/yaml.v3#section-readme
- m := make(map[interface{}]interface{})
-
- err = yaml.Unmarshal(marshaled, &m)
- if err != nil {
- return nil, fmt.Errorf("failed to unmarshal YAML: %v", err)
- }
-
- b, err := yaml.Marshal(m)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal back to YAML: %v", err)
- }
-
- formattedYAML := reduceIndentation(b, 2)
-
- return fmt.Sprintf("---\n%s", string(formattedYAML)), nil
-}
-
-func reduceIndentation(b []byte, n int) []byte {
- lines := bytes.Split(b, []byte("\n"))
-
- for i, line := range lines {
- // Replace tabs with spaces for consistent processing
- line = bytes.ReplaceAll(line, []byte("\t"), []byte(" "))
-
- trimmedLine := bytes.TrimLeft(line, " ")
- indentation := len(line) - len(trimmedLine)
- if indentation >= n {
- lines[i] = bytes.TrimPrefix(line, bytes.Repeat([]byte(" "), n))
- } else {
- lines[i] = trimmedLine
- }
- }
-
- return bytes.Join(lines, []byte("\n"))
-}
-
All tests pass
❯ go test ./...
? github.com/facebookincubator/ttpforge [no test files]
ok github.com/facebookincubator/ttpforge/cmd 0.373s
ok github.com/facebookincubator/ttpforge/pkg/blocks 0.196s
ok github.com/facebookincubator/ttpforge/pkg/files 0.285s
ok github.com/facebookincubator/ttpforge/pkg/logging (cached)
ok github.com/facebookincubator/ttpforge/pkg/strings (cached)
Sanity check - I'm on latest commit of the PR branch:
❯ git rev-parse --abbrev-ref HEAD
i20
❯ git rev-parse HEAD
67ed7c0107b6a80a5592585928480fc33041534d
|
||
func createTTP() (*blocks.TTP, error) { | ||
ttp := &blocks.TTP{ | ||
Name: filepath.Base(newTTPInput.Path), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for the future - would be preferable to restrict the global var newTTPInput
to just being used in the cobra flag parsing, then make the instance of NewTTPInput
a function parameter of all these other functions, instead of using the global var everywhere - global vars in general cause lots of problems for testability/maintainability
|
||
err = runCmd.Execute() | ||
require.NoError(t, err, fmt.Sprintf("failed to run TTP: %v", err)) | ||
output := captureStdout(t, func() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good stuff - exactly what I was looking for
Just to clarify re the last comment
Yup - my comment there referenced an earlier version of the PR |
Proposed Changes
Related Issue(s)
Testing
Run the following:
Documentation
N/A
Screenshots/GIFs (optional)
N/A
Checklist
mage runprecommit
locally and fixed any issues that arose.mage runtests
locally and fixed any issues that arose.