├── version ├── cmd ├── main.go └── root.go ├── docs ├── technique-extended.png └── technique-overview.png ├── internal ├── models │ ├── main.go │ ├── actor_test.go │ ├── utils_test.go │ ├── mitigation.go │ ├── definition.go │ ├── control.go │ ├── data_component.go │ ├── identity.go │ ├── tactic.go │ ├── data_source.go │ ├── relationship.go │ ├── matrix.go │ ├── campaign.go │ ├── utils.go │ ├── actor.go │ ├── technique.go │ ├── malware.go │ ├── tool.go │ ├── base.go │ └── models_test.go ├── menu │ ├── tab.go │ ├── utils.go │ ├── main.go │ └── model.go └── logger │ └── main.go ├── enterprise_test.go ├── .gitignore ├── tests └── data │ ├── definition.example.json │ ├── data_component.example.json │ ├── relationship.example.json │ ├── tactic.example.json │ ├── mitigation.example.json │ ├── data_source.example.json │ ├── matrix.example.json │ ├── malware.example.json │ ├── actor.example.json │ ├── tool.example.json │ └── campaign.example.json ├── goattck_test.go ├── go.mod ├── storage.go ├── README.md ├── go.sum └── enterprise.go /version: -------------------------------------------------------------------------------- 1 | 0.2.0 -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func main() { 4 | Execute() 5 | } 6 | -------------------------------------------------------------------------------- /docs/technique-extended.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSAdministrator/goattck/HEAD/docs/technique-extended.png -------------------------------------------------------------------------------- /docs/technique-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MSAdministrator/goattck/HEAD/docs/technique-overview.png -------------------------------------------------------------------------------- /internal/models/main.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "github.com/msadministrator/goattck/internal/logger" 4 | 5 | var ( 6 | slogger = logger.NewLogger(logger.Info) 7 | ) 8 | -------------------------------------------------------------------------------- /enterprise_test.go: -------------------------------------------------------------------------------- 1 | package goattck_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/msadministrator/goattck" 9 | ) 10 | 11 | func TestEnterprise_buildRelationshipMap(t *testing.T) { 12 | enterprise, err := goattck.Enterprise{}.New(attackURL) 13 | assert.Nil(t, err) 14 | enterprise, err = enterprise.Load(false) 15 | assert.Nil(t, err) 16 | assert.NotNil(t, enterprise) 17 | 18 | assert.Equal(t, 181, len(enterprise.Actors)) 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | enterprise-attack.json 24 | .DS_Store -------------------------------------------------------------------------------- /tests/data/definition.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "marking-definition", 3 | "id": "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168", 4 | "created": "2017-06-01T00:00:00.000Z", 5 | "definition": { 6 | "statement": "Copyright 2015-2023, The MITRE Corporation. MITRE ATT&CK and ATT&CK are registered trademarks of The MITRE Corporation." 7 | }, 8 | "definition_type": "statement", 9 | "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 10 | "x_mitre_attack_spec_version": "2.1.0", 11 | "object_marking_refs": [], 12 | "external_references": [], 13 | "revoked": false 14 | } -------------------------------------------------------------------------------- /internal/models/actor_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewActor(t *testing.T) { 8 | actor, err := NewActor(loadTestJSON("actor.example.json")) 9 | if err != nil { 10 | t.Errorf("Error, could not load Actor: %v", err) 11 | } 12 | if actor.Name != "APT1" { 13 | t.Errorf("Error, could not load Actor data models: %v", err) 14 | } 15 | if actor.XMitreVersion != "1.4" { 16 | t.Errorf("Error, could not load Actor data models: %v", err) 17 | } 18 | if actor.ExternalReferences[0].SourceName != "mitre-attack" { 19 | t.Errorf("Error, could not load Actor data models: %v", err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/models/utils_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | // jsonToMap converts a json string to a map 10 | func jsonToMap(jsonStr string) map[string]interface{} { 11 | result := make(map[string]interface{}) 12 | json.Unmarshal([]byte(jsonStr), &result) 13 | return result 14 | } 15 | 16 | // loadTestJSON loads a json file from the tests/data directory 17 | func loadTestJSON(fileName string) map[string]interface{} { 18 | content, err := os.ReadFile(fmt.Sprintf("../../tests/data/%s", fileName)) 19 | if err != nil { 20 | fmt.Println(err) 21 | } 22 | return jsonToMap(string(content)) 23 | } 24 | -------------------------------------------------------------------------------- /internal/models/mitigation.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "encoding/json" 4 | 5 | type mitigation interface{} 6 | 7 | type Mitigation struct { 8 | BaseModel 9 | BaseAttributes 10 | // These are properties from the MITRE ATT&CK json 11 | ExternalReferences []ExternalReference `json:"external_references"` 12 | XMitreModifiedByRef string `json:"x_mitre_modified_by_ref"` 13 | XMitreAttackSpecVersion string `json:"x_mitre_attack_spec_version,omitempty"` 14 | Techniques []Technique 15 | } 16 | 17 | var _ (mitigation) = new(Mitigation) 18 | 19 | func NewMitigation(object map[string]interface{}) (Mitigation, error) { 20 | mitigation := Mitigation{} 21 | jsonString, _ := json.Marshal(object) 22 | json.Unmarshal(jsonString, &mitigation) 23 | return mitigation, nil 24 | } 25 | -------------------------------------------------------------------------------- /internal/models/definition.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "encoding/json" 4 | 5 | type markingDefinition interface { 6 | } 7 | 8 | type MarkingDefinition struct { 9 | BaseModel 10 | // These are properties from the MITRE ATT&CK json 11 | Definition struct { 12 | Statement string `json:"statement"` 13 | } `json:"definition"` 14 | CreatedByRef string `json:"created_by_ref"` 15 | DefinitionType string `json:"definition_type"` 16 | XMitreAttackSpecVersion string `json:"x_mitre_attack_spec_version"` 17 | } 18 | 19 | var _ (markingDefinition) = new(MarkingDefinition) 20 | 21 | func NewMarkingDefinition(object map[string]interface{}) (MarkingDefinition, error) { 22 | definition := MarkingDefinition{} 23 | jsonString, _ := json.Marshal(object) 24 | json.Unmarshal(jsonString, &definition) 25 | return definition, nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/models/control.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "encoding/json" 4 | 5 | type control interface { 6 | } 7 | 8 | type Control struct { 9 | // Base fields 10 | BaseModel 11 | // Fields 12 | Revoked bool `json:"revoked"` 13 | XMitreFamily string `json:"x_mitre_family"` 14 | XMitreImpact []string `json:"x_mitre_impact"` 15 | XMitrePriority string `json:"x_mitre_priority"` 16 | ObjectMarkingRefs []string `json:"object_marking_refs"` 17 | ExternalReferences []ExternalReference `json:"external_references"` 18 | Techniques []Technique 19 | } 20 | 21 | var _ (control) = new(Control) 22 | 23 | func NewControl(object map[string]interface{}) (Control, error) { 24 | control := Control{} 25 | jsonString, _ := json.Marshal(object) 26 | json.Unmarshal(jsonString, &control) 27 | return control, nil 28 | } 29 | -------------------------------------------------------------------------------- /internal/models/data_component.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type dataComponent interface { 8 | } 9 | 10 | type DataComponent struct { 11 | BaseModel 12 | BaseAttributes 13 | // These are properties from the MITRE ATT&CK json 14 | XMitreDataSourceRef string `json:"x_mitre_data_source_ref"` 15 | Type string `json:"type"` 16 | XMitreAttackSpecVersion string `json:"x_mitre_attack_spec_version"` 17 | XMitreModifiedByRef string `json:"x_mitre_modified_by_ref"` 18 | Techniques []Technique 19 | } 20 | 21 | var _ (dataComponent) = new(DataComponent) 22 | 23 | func NewDataComponent(object map[string]interface{}) (DataComponent, error) { 24 | dataComponent := DataComponent{} 25 | jsonString, _ := json.Marshal(object) 26 | json.Unmarshal(jsonString, &dataComponent) 27 | return dataComponent, nil 28 | } 29 | 30 | func (dc *DataComponent) GetExternalID() string { 31 | return "" 32 | } 33 | -------------------------------------------------------------------------------- /internal/models/identity.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type identity interface{} 8 | 9 | type Identity struct { 10 | BaseModel 11 | // These are properties from the MITRE ATT&CK json 12 | ObjectMarkingRefs []string `json:"object_marking_refs"` 13 | IdentityClass string `json:"identity_class"` 14 | } 15 | 16 | var _ (identity) = new(Identity) 17 | 18 | func NewIdentity(object map[string]interface{}) (Identity, error) { 19 | identity := Identity{} 20 | baseModel, err := parseBaseModel(object) 21 | if err != nil { 22 | slogger.Error(fmt.Sprintf("Error parsing base model: %s", err)) 23 | } 24 | identity.BaseModel = baseModel 25 | if object["object_marking_refs"] != nil { 26 | identity.ObjectMarkingRefs = ConvertInterfaceArrayToStringArray(object["object_marking_refs"].([]interface{})) 27 | } 28 | if object["identity_class"] != nil { 29 | identity.IdentityClass = object["identity_class"].(string) 30 | } 31 | return identity, nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/menu/tab.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/charmbracelet/lipgloss" 5 | ) 6 | 7 | var ( 8 | overviewTab = mitreTab{name: "Overview"} 9 | relationshipsTab = mitreTab{name: "Relationships"} 10 | referencesTab = mitreTab{name: "References"} 11 | extendedTab = mitreTab{name: "Extended"} 12 | externalTab = mitreTab{name: "External"} 13 | 14 | mitreTabs = []mitreTab{overviewTab, relationshipsTab, referencesTab, extendedTab, externalTab} 15 | ) 16 | 17 | type tabName string 18 | 19 | type mitreTab struct { 20 | name tabName 21 | } 22 | 23 | func (t mitreTab) GetRow() string { 24 | var otherTabs []string 25 | 26 | for _, tabName := range mitreTabs { 27 | if tabName.name != t.name { 28 | otherTabs = append(otherTabs, tab.Render(string(tabName.name))) 29 | } else { 30 | otherTabs = append(otherTabs, activeTab.Render(string(t.name))) 31 | } 32 | } 33 | 34 | return lipgloss.JoinHorizontal( 35 | lipgloss.Top, 36 | otherTabs..., 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /internal/models/tactic.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "encoding/json" 4 | 5 | type tactic interface{} 6 | 7 | type Tactic struct { 8 | BaseModel 9 | // These are properties from the MITRE ATT&CK json 10 | ObjectMarkingRefs []string `json:"object_marking_refs"` 11 | CreatedByRef string `json:"created_by_ref"` 12 | ExternalReferences []ExternalReference `json:"external_references"` 13 | Description string `json:"description"` 14 | XMitreAttackSpecVersion string `json:"x_mitre_attack_spec_version"` 15 | XMitreModifiedByRef string `json:"x_mitre_modified_by_ref"` 16 | XMitreShortname string `json:"x_mitre_shortname"` 17 | Techniques []Technique 18 | } 19 | 20 | var _ (tactic) = new(Tactic) 21 | 22 | func NewTactic(object map[string]interface{}) (Tactic, error) { 23 | tactic := Tactic{} 24 | jsonString, _ := json.Marshal(object) 25 | json.Unmarshal(jsonString, &tactic) 26 | return tactic, nil 27 | } 28 | -------------------------------------------------------------------------------- /tests/data/data_component.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "x-mitre-data-component--a953ca55-921a-44f7-9b8d-3d40141aa17e", 3 | "name": "User Account Authentication", 4 | "created": "2021-10-20T15:05:19.271Z", 5 | "modified": "2022-10-07T16:19:46.282Z", 6 | "x_mitre_version": "1.1", 7 | "type": "x-mitre-data-component", 8 | "description": "An attempt by a user to gain access to a network or computing resource, often by providing credentials (ex: Windows EID 4776 or /var/log/auth.log)", 9 | "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 10 | "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 11 | "x_mitre_data_source_ref": "x-mitre-data-source--0b4f86ed-f4ab-46a3-8ed1-175be1974da6", 12 | "object_marking_refs": [ 13 | "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" 14 | ], 15 | "x_mitre_domains": [], 16 | "x_mitre_attack_spec_version": "2.1.0", 17 | "x_mitre_deprecated": false, 18 | "revoked": false, 19 | "external_references": [] 20 | } -------------------------------------------------------------------------------- /internal/models/data_source.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "encoding/json" 4 | 5 | type dataSource interface { 6 | } 7 | 8 | type DataSource struct { 9 | BaseModel 10 | BaseAttributes 11 | BaseExternalModel 12 | // These are properties from the MITRE ATT&CK json 13 | XMitrePlatforms []string `json:"x_mitre_platforms"` 14 | XMitreContributors []string `json:"x_mitre_contributors,omitempty"` 15 | XMitreCollectionLayers []string `json:"x_mitre_collection_layers"` 16 | ExternalReferences []ExternalReference `json:"external_references"` 17 | XMitreAttackSpecVersion string `json:"x_mitre_attack_spec_version"` 18 | XMitreModifiedByRef string `json:"x_mitre_modified_by_ref"` 19 | DataComponents []DataComponent 20 | Techniques []Technique 21 | } 22 | 23 | var _ (dataSource) = new(DataSource) 24 | 25 | func NewDataSource(object map[string]interface{}) (DataSource, error) { 26 | dataSource := DataSource{} 27 | jsonString, _ := json.Marshal(object) 28 | json.Unmarshal(jsonString, &dataSource) 29 | return dataSource, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/models/relationship.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "encoding/json" 4 | 5 | type relationship interface{} 6 | 7 | type Relationship struct { 8 | BaseAttributes 9 | Id string `json:"id"` 10 | Type string `json:"type"` 11 | Created string `json:"created"` 12 | Modified string `json:"modified"` 13 | SourceRef string `json:"source_ref"` 14 | TargetRef string `json:"target_ref"` 15 | RelationshipType string `json:"relationship_type"` 16 | XMitreVersion string `json:"x_mitre_version"` 17 | XMitreAttackSpecVersion string `json:"x_mitre_attack_spec_version"` 18 | ExternalReferences []ExternalReference `json:"external_references"` 19 | XMitreModifiedByRef string `json:"x_mitre_modified_by_ref"` 20 | } 21 | 22 | var _ (relationship) = new(Relationship) 23 | 24 | func NewRelationship(object map[string]interface{}) (Relationship, error) { 25 | relationship := Relationship{} 26 | jsonString, _ := json.Marshal(object) 27 | json.Unmarshal(jsonString, &relationship) 28 | return relationship, nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/models/matrix.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "encoding/json" 4 | 5 | type matrix interface{} 6 | 7 | type Matrix struct { 8 | BaseModel 9 | Type string `json:"type"` 10 | TacticRefs []string `json:"tactic_refs"` 11 | CreatedByRef string `json:"created_by_ref"` 12 | Description string `json:"description"` 13 | Revoked bool `json:"revoked"` 14 | XMitreDomains []string `json:"x_mitre_domains"` 15 | ObjectMarkingRefs []string `json:"object_marking_refs"` 16 | ExternalReferences []ExternalReference `json:"external_references"` 17 | XMitreDeprecated bool `json:"x_mitre_deprecated"` 18 | XMitreVersion string `json:"x_mitre_version"` 19 | XMitreModifiedByRef string `json:"x_mitre_modified_by_ref"` 20 | XMitreAttackSpecVersion string `json:"x_mitre_attack_spec_version"` 21 | } 22 | 23 | var _ (matrix) = new(Matrix) 24 | 25 | func NewMatrix(object map[string]interface{}) (Matrix, error) { 26 | matrix := Matrix{} 27 | jsonString, _ := json.Marshal(object) 28 | json.Unmarshal(jsonString, &matrix) 29 | return matrix, nil 30 | } 31 | -------------------------------------------------------------------------------- /internal/models/campaign.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type campaign interface { 10 | } 11 | 12 | type Campaign struct { 13 | BaseModel 14 | BaseAttributes 15 | BaseExternalModel 16 | // These are properties from the MITRE ATT&CK json 17 | FirstSeen time.Time `json:"first_seen"` 18 | LastSeen time.Time `json:"last_seen"` 19 | XMitreFirstSeenCitation string `json:"x_mitre_first_seen_citation"` 20 | XMitreLastSeenCitation string `json:"x_mitre_last_seen_citation"` 21 | XMitreContributors []string `json:"x_mitre_contributors,omitempty"` 22 | Malwares []Malware 23 | Techniques []Technique 24 | Tools []Tool 25 | } 26 | 27 | var _ (campaign) = new(Campaign) 28 | 29 | func NewCampaign(object map[string]interface{}) (Campaign, error) { 30 | campaign := Campaign{} 31 | jsonString, _ := json.Marshal(object) 32 | json.Unmarshal(jsonString, &campaign) 33 | return campaign, nil 34 | } 35 | 36 | func (m *Campaign) GetExternalID() string { 37 | mitreID := "" 38 | for _, ref := range m.ExternalReferences { 39 | if ref.ExternalId != "" && mitreID == "" { 40 | if strings.HasPrefix(ref.ExternalId, "C") { 41 | mitreID = ref.ExternalId 42 | } 43 | } 44 | } 45 | return mitreID 46 | } 47 | -------------------------------------------------------------------------------- /goattck_test.go: -------------------------------------------------------------------------------- 1 | package goattck_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/msadministrator/goattck" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | const attackURL = "https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json" 11 | 12 | func TestEnterprise_New(t *testing.T) { 13 | enterprise, err := goattck.Enterprise{}.New(attackURL) 14 | if err != nil { 15 | t.Errorf("Error, could not load Enterprise: %v", err) 16 | } 17 | assert.IsType(t, goattck.Enterprise{}, enterprise) 18 | enterprise, err = enterprise.Load(false) 19 | assert.Nil(t, err) 20 | assert.Greater(t, len(enterprise.Actors), 20) 21 | assert.Greater(t, len(enterprise.Campaigns), 5) 22 | assert.Greater(t, len(enterprise.DataComponents), 5) 23 | assert.Greater(t, len(enterprise.DataSources), 5) 24 | assert.Equal(t, len(enterprise.Defintions), 1) 25 | assert.Greater(t, len(enterprise.Malwares), 20) 26 | assert.Equal(t, len(enterprise.Matrices), 1) 27 | assert.Greater(t, len(enterprise.Mitigations), 5) 28 | assert.Greater(t, len(enterprise.Relationships), 5) 29 | assert.Equal(t, len(enterprise.Tactics), 14) 30 | assert.Greater(t, len(enterprise.Techniques), 200) 31 | assert.Greater(t, len(enterprise.Tools), 20) 32 | 33 | fakeURL := "hxxps://test.test.test/enterprise-legacy/enterprise-legacy.json" 34 | e, err := goattck.Enterprise{}.New(fakeURL) 35 | assert.NotNil(t, e) 36 | assert.Nil(t, err) 37 | } 38 | -------------------------------------------------------------------------------- /tests/data/relationship.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "relationship--74f14668-7111-4f96-a307-4aac00d91cf4", 3 | "type": "relationship", 4 | "created": "2020-05-01T20:05:16.006Z", 5 | "modified": "2020-05-04T19:13:35.556Z", 6 | "source_ref": "malware--aad11e34-02ca-4220-91cd-2ed420af4db3", 7 | "target_ref": "attack-pattern--deb98323-e13f-4b0c-8d94-175379069062", 8 | "relationship_type": "uses", 9 | "object_marking_refs": [ 10 | "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" 11 | ], 12 | "revoked": false, 13 | "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 14 | "description": "[HotCroissant](https://attack.mitre.org/software/S0431) has used the open source UPX executable packer.(Citation: Carbon Black HotCroissant April 2020)", 15 | "x_mitre_deprecated": false, 16 | "x_mitre_version": "1.0", 17 | "x_mitre_attack_spec_version": "", 18 | "external_references": [ 19 | { 20 | "source_name": "Carbon Black HotCroissant April 2020", 21 | "url": "https://www.carbonblack.com/2020/04/16/vmware-carbon-black-tau-threat-analysis-the-evolution-of-lazarus/", 22 | "external_id": "", 23 | "description": "Knight, S.. (2020, April 16). VMware Carbon Black TAU Threat Analysis: The Evolution of Lazarus. Retrieved May 1, 2020." 24 | } 25 | ], 26 | "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5" 27 | } -------------------------------------------------------------------------------- /internal/models/utils.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | var ( 10 | errorLoadingJSON = errors.New("error loading JSON from disk.") 11 | ) 12 | 13 | func ConvertInterfaceArrayToStringArray(aInterface []interface{}) []string { 14 | aString := make([]string, len(aInterface)) 15 | for i, v := range aInterface { 16 | aString[i] = v.(string) 17 | } 18 | return aString 19 | } 20 | 21 | func ObjectAssign(target interface{}, object interface{}) { 22 | // object atributes values in target atributes values 23 | // using pattern matching (https://golang.org/pkg/reflect/#Value.FieldByName) 24 | // https://stackoverflow.com/questions/35590190/how-to-use-the-spread-operator-in-golang 25 | t := reflect.ValueOf(target).Elem() 26 | o := reflect.ValueOf(object).Elem() 27 | for i := 0; i < o.NumField(); i++ { 28 | for j := 0; j < t.NumField(); j++ { 29 | if t.Field(j) == o.Field(i) { 30 | fmt.Printf("Field %s is equal to %s\n", t.Field(j), o.Field(i)) 31 | t.Field(j).Set(o.Field(i)) 32 | } 33 | } 34 | } 35 | } 36 | 37 | func IsStructEmpty(object interface{}) (bool, error) { 38 | if reflect.ValueOf(object).Kind() == reflect.Struct { 39 | // and create an empty copy of the struct object to compare against 40 | empty := reflect.New(reflect.TypeOf(object)).Elem().Interface() 41 | if reflect.DeepEqual(object, empty) { 42 | return true, nil 43 | } else { 44 | return false, nil 45 | } 46 | } 47 | return false, nil 48 | } 49 | -------------------------------------------------------------------------------- /tests/data/tactic.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "x-mitre-tactic--2558fd61-8c75-4730-94c4-11926db2a263", 3 | "name": "Credential Access", 4 | "created": "2018-10-17T00:14:20.652Z", 5 | "modified": "2019-07-19T17:43:41.967Z", 6 | "type": "x-mitre-tactic", 7 | "description": "The adversary is trying to steal account names and passwords.\n\nCredential Access consists of techniques for stealing credentials like account names and passwords. Techniques used to get credentials include keylogging or credential dumping. Using legitimate credentials can give adversaries access to systems, make them harder to detect, and provide the opportunity to create more accounts to help achieve their goals.", 8 | "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 9 | "x_mitre_shortname": "credential-access", 10 | "object_marking_refs": [ 11 | "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" 12 | ], 13 | "external_references": [ 14 | { 15 | "source_name": "mitre-attack", 16 | "url": "https://attack.mitre.org/tactics/TA0006", 17 | "external_id": "TA0006", 18 | "description": "" 19 | } 20 | ], 21 | "x_mitre_contributors": [], 22 | "x_mitre_deprecated": false, 23 | "revoked": false, 24 | "x_mitre_version": "1.0", 25 | "x_mitre_domains": [ 26 | "enterprise-attack" 27 | ], 28 | "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 29 | "x_mitre_attack_spec_version": "2.1.0" 30 | } -------------------------------------------------------------------------------- /tests/data/mitigation.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "course-of-action--13cad982-35e3-4340-9095-7124b653df4b", 3 | "name": "Data from Information Repositories Mitigation", 4 | "created": "2018-10-17T00:14:20.652Z", 5 | "modified": "2019-07-24T19:06:19.932Z", 6 | "x_mitre_version": "1.0", 7 | "type": "course-of-action", 8 | "description": "To mitigate adversary access to information repositories for collection:\n\n* Develop and publish policies that define acceptable information to be stored\n* Appropriate implementation of access control mechanisms that include both authentication and appropriate authorization\n* Enforce the principle of least-privilege\n* Periodic privilege review of accounts\n* Mitigate access to [Valid Accounts](https://attack.mitre.org/techniques/T1078) that may be used to access repositories", 9 | "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 10 | "x_mitre_deprecated": true, 11 | "object_marking_refs": [ 12 | "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" 13 | ], 14 | "external_references": [ 15 | { 16 | "source_name": "mitre-attack", 17 | "url": "https://attack.mitre.org/mitigations/T1213", 18 | "external_id": "T1213", 19 | "description": "" 20 | } 21 | ], 22 | "mitigation_id": "", 23 | "labels": [], 24 | "x_mitre_attack_spec_version": "", 25 | "x_mitre_old_attack_id": "", 26 | "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 27 | "x_mitre_domains": [ 28 | "enterprise-attack" 29 | ], 30 | "revoked": false 31 | } -------------------------------------------------------------------------------- /tests/data/data_source.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "x-mitre-data-source--0b4f86ed-f4ab-46a3-8ed1-175be1974da6", 3 | "name": "User Account", 4 | "created": "2021-10-20T15:05:19.271Z", 5 | "modified": "2022-12-07T19:50:43.993Z", 6 | "x_mitre_version": "1.1", 7 | "type": "x-mitre-data-source", 8 | "description": "A profile representing a user, device, service, or application used to authenticate and access resources", 9 | "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 10 | "x_mitre_attack_spec_version": "3.1.0", 11 | "x_mitre_collection_layers": [ 12 | "Cloud Control Plane", 13 | "Container", 14 | "Host" 15 | ], 16 | "x_mitre_domains": [ 17 | "enterprise-attack" 18 | ], 19 | "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 20 | "external_references": [ 21 | { 22 | "source_name": "mitre-attack", 23 | "url": "https://attack.mitre.org/datasources/DS0002", 24 | "external_id": "DS0002", 25 | "description": "" 26 | } 27 | ], 28 | "object_marking_refs": [ 29 | "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" 30 | ], 31 | "aliases": [], 32 | "revoked": false, 33 | "x_mitre_deprecated": false, 34 | "x_mitre_contributors": [ 35 | "Center for Threat-Informed Defense (CTID)" 36 | ], 37 | "x_mitre_platforms": [ 38 | "Azure AD", 39 | "Containers", 40 | "Google Workspace", 41 | "IaaS", 42 | "Linux", 43 | "Office 365", 44 | "SaaS", 45 | "Windows", 46 | "macOS" 47 | ] 48 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/msadministrator/goattck 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.0 6 | 7 | require ( 8 | github.com/charmbracelet/bubbles v0.21.0 9 | github.com/charmbracelet/bubbletea v1.3.4 10 | github.com/charmbracelet/lipgloss v1.1.0 11 | github.com/lucasb-eyer/go-colorful v1.2.0 12 | github.com/spf13/cobra v1.7.0 13 | github.com/stretchr/testify v1.8.4 14 | golang.org/x/term v0.31.0 15 | ) 16 | 17 | require ( 18 | github.com/atotto/clipboard v0.1.4 // indirect 19 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 20 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect 21 | github.com/charmbracelet/x/ansi v0.8.0 // indirect 22 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect 23 | github.com/charmbracelet/x/term v0.2.1 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 26 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 27 | github.com/mattn/go-isatty v0.0.20 // indirect 28 | github.com/mattn/go-localereader v0.0.1 // indirect 29 | github.com/mattn/go-runewidth v0.0.16 // indirect 30 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 31 | github.com/muesli/cancelreader v0.2.2 // indirect 32 | github.com/muesli/termenv v0.16.0 // indirect 33 | github.com/pmezard/go-difflib v1.0.0 // indirect 34 | github.com/rivo/uniseg v0.4.7 // indirect 35 | github.com/sahilm/fuzzy v0.1.1 // indirect 36 | github.com/spf13/pflag v1.0.5 // indirect 37 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 38 | golang.org/x/sync v0.11.0 // indirect 39 | golang.org/x/sys v0.32.0 // indirect 40 | golang.org/x/text v0.3.8 // indirect 41 | gopkg.in/yaml.v3 v3.0.1 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /internal/models/actor.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | ) 7 | 8 | type actor interface { 9 | } 10 | 11 | // ActorObject is a representation of the MITRE ATT&CK Actor json model 12 | type Actor struct { 13 | BaseModel 14 | BaseAttributes 15 | BaseExternalModel 16 | // These are properties from the MITRE ATT&CK json 17 | XMitreContributors []string `json:"x_mitre_contributors,omitempty"` 18 | // These are properties unique to pyattck-data 19 | actorExternalAttributes 20 | MitreAttckId string `json:"mitre_attck_id"` 21 | Malwares []Malware 22 | Techniques []Technique 23 | Tools []Tool 24 | } 25 | 26 | var _ (actor) = new(Actor) 27 | 28 | // actorExternalAttributes are properties external from the MITRE ATT&CK json definitions 29 | type actorExternalAttributes struct { 30 | Names []string `json:"names"` 31 | ExternalTools []string `json:"external_tools"` 32 | Country []string `json:"country"` 33 | Operations []string `json:"operations"` 34 | Links []string `json:"links"` 35 | Targets []string `json:"targets"` 36 | ExternalDescription []string `json:"external_description"` 37 | AttckID string `json:"attck_id"` 38 | Comment string `json:"comment"` 39 | Comments []string `json:"comments"` 40 | } 41 | 42 | // NewActor is a function that takes in a map of data and returns a ActorObject 43 | func NewActor(object map[string]interface{}) (Actor, error) { 44 | actor := Actor{} 45 | jsonString, _ := json.Marshal(object) 46 | json.Unmarshal(jsonString, &actor) 47 | return actor, nil 48 | } 49 | 50 | func (a *Actor) GetExternalID() string { 51 | mitreID := "" 52 | for _, ref := range a.ExternalReferences { 53 | if ref.ExternalId != "" && mitreID == "" { 54 | if strings.HasPrefix(ref.ExternalId, "G") { 55 | mitreID = ref.ExternalId 56 | } 57 | } 58 | } 59 | return mitreID 60 | } 61 | -------------------------------------------------------------------------------- /internal/menu/utils.go: -------------------------------------------------------------------------------- 1 | // Contains utilities for the menu compoonent 2 | package menu 3 | 4 | import ( 5 | "image/color" 6 | 7 | "github.com/charmbracelet/lipgloss" 8 | "github.com/charmbracelet/lipgloss/table" 9 | lipglosstable "github.com/charmbracelet/lipgloss/table" 10 | "github.com/lucasb-eyer/go-colorful" 11 | ) 12 | 13 | func max(a, b int) int { 14 | if a > b { 15 | return a 16 | } 17 | return b 18 | } 19 | 20 | func colorGrid(xSteps, ySteps int) [][]string { 21 | x0y0, _ := colorful.Hex("#F25D94") 22 | x1y0, _ := colorful.Hex("#EDFF82") 23 | x0y1, _ := colorful.Hex("#643AFF") 24 | x1y1, _ := colorful.Hex("#14F9D5") 25 | 26 | x0 := make([]colorful.Color, ySteps) 27 | for i := range x0 { 28 | x0[i] = x0y0.BlendLuv(x0y1, float64(i)/float64(ySteps)) 29 | } 30 | 31 | x1 := make([]colorful.Color, ySteps) 32 | for i := range x1 { 33 | x1[i] = x1y0.BlendLuv(x1y1, float64(i)/float64(ySteps)) 34 | } 35 | 36 | grid := make([][]string, ySteps) 37 | for x := 0; x < ySteps; x++ { 38 | y0 := x0[x] 39 | grid[x] = make([]string, xSteps) 40 | for y := 0; y < xSteps; y++ { 41 | grid[x][y] = y0.BlendLuv(x1[x], float64(y)/float64(xSteps)).Hex() 42 | } 43 | } 44 | 45 | return grid 46 | } 47 | 48 | func rainbow(base lipgloss.Style, s string, colors []color.Color) string { 49 | var str string 50 | for i, ss := range s { 51 | color, _ := colorful.MakeColor(colors[i%len(colors)]) 52 | str = str + base.Foreground(lipgloss.Color(color.Hex())).Render(string(ss)) 53 | } 54 | return str 55 | } 56 | 57 | func getFormattedTable(headers []string, rows [][]string) string { 58 | return lipglosstable.New(). 59 | Border(lipgloss.NormalBorder()). 60 | BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))). 61 | StyleFunc(func(row, col int) lipgloss.Style { 62 | switch { 63 | case row == table.HeaderRow: 64 | return lipgloss.NewStyle().Foreground(lipgloss.Color("99")).Bold(true).Align(lipgloss.Center) 65 | default: 66 | return lipgloss.NewStyle().Padding(0, 1).Width(width / len(headers)).Foreground(lipgloss.Color("245")) 67 | } 68 | }). 69 | Headers(headers...). 70 | Rows(rows...). 71 | String() 72 | } 73 | -------------------------------------------------------------------------------- /tests/data/matrix.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "x-mitre-matrix--eafc1b4c-5e56-4965-bd4e-66a6a89c88cc", 3 | "name": "Enterprise ATT&CK", 4 | "created": "2018-10-17T00:14:20.652Z", 5 | "modified": "2022-04-01T20:43:55.937Z", 6 | "type": "x-mitre-matrix", 7 | "tactic_refs": [ 8 | "x-mitre-tactic--daa4cbb1-b4f4-4723-a824-7f1efd6e0592", 9 | "x-mitre-tactic--d679bca2-e57d-4935-8650-8031c87a4400", 10 | "x-mitre-tactic--ffd5bcee-6e16-4dd2-8eca-7b3beedf33ca", 11 | "x-mitre-tactic--4ca45d45-df4d-4613-8980-bac22d278fa5", 12 | "x-mitre-tactic--5bc1d813-693e-4823-9961-abf9af4b0e92", 13 | "x-mitre-tactic--5e29b093-294e-49e9-a803-dab3d73b77dd", 14 | "x-mitre-tactic--78b23412-0651-46d7-a540-170a1ce8bd5a", 15 | "x-mitre-tactic--2558fd61-8c75-4730-94c4-11926db2a263", 16 | "x-mitre-tactic--c17c5845-175e-4421-9713-829d0573dbc9", 17 | "x-mitre-tactic--7141578b-e50b-4dcc-bfa4-08a8dd689e9e", 18 | "x-mitre-tactic--d108ce10-2419-4cf9-a774-46161d6c6cfe", 19 | "x-mitre-tactic--f72804c5-f15a-449e-a5da-2eecd181f813", 20 | "x-mitre-tactic--9a4e74ab-5008-408c-84bf-a10dfbc53462", 21 | "x-mitre-tactic--5569339b-94c2-49ee-afb3-2222936582c8" 22 | ], 23 | "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 24 | "description": "Below are the tactics and technique representing the MITRE ATT&CK Matrix for Enterprise. The Matrix contains information for the following platforms: Windows, macOS, Linux, AWS, GCP, Azure, Azure AD, Office 365, SaaS.", 25 | "revoked": false, 26 | "x_mitre_domains": [], 27 | "object_marking_refs": [ 28 | "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" 29 | ], 30 | "external_references": [ 31 | { 32 | "source_name": "mitre-attack", 33 | "url": "https://attack.mitre.org/matrices/enterprise", 34 | "external_id": "enterprise-attack", 35 | "description": "" 36 | } 37 | ], 38 | "x_mitre_deprecated": false, 39 | "x_mitre_version": "1.0", 40 | "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 41 | "x_mitre_attack_spec_version": "2.1.0" 42 | } -------------------------------------------------------------------------------- /internal/logger/main.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type LogLevel int 12 | 13 | const ( 14 | Info LogLevel = iota 15 | Debug 16 | Warning 17 | Error 18 | Fatal 19 | ) 20 | 21 | var levelNames = map[LogLevel]string{ 22 | Info: "INFO", 23 | Debug: "DEBUG", 24 | Warning: "WARN", 25 | Error: "ERROR", 26 | Fatal: "FATAL", 27 | } 28 | 29 | // ParseLogLevel converts a string to a LogLevel. 30 | func ParseLogLevel(level string) LogLevel { 31 | level = strings.ToLower(strings.TrimSpace(level)) 32 | switch level { 33 | case "info": 34 | return Info 35 | case "debug": 36 | return Debug 37 | case "warn", "warning": 38 | return Warning 39 | case "error": 40 | return Error 41 | case "fatal": 42 | return Fatal 43 | default: 44 | return Info // Default to Info if the input is invalid. 45 | } 46 | } 47 | 48 | // Logger struct with configurable output destination. 49 | type Logger struct { 50 | level LogLevel 51 | logger *log.Logger 52 | } 53 | 54 | // NewLogger initializes a logger 55 | func NewLogger(level LogLevel) *Logger { 56 | flags := log.LstdFlags | log.Lshortfile 57 | logger := log.New(os.Stdout, "", flags) 58 | 59 | return &Logger{level: level, logger: logger} 60 | } 61 | 62 | // formatMessage creates a log message with a timestamp and level. 63 | func (l *Logger) formatMessage(level LogLevel, message string, a ...any) string { 64 | timestamp := time.Now().Format("2006-01-02T15:04:05-0700") 65 | prefix := fmt.Sprintf("[%s] %s: ", levelNames[level], timestamp) 66 | return prefix + fmt.Sprintf(message, a...) 67 | } 68 | 69 | // logMessage writes a formatted message to the log. 70 | func (l *Logger) logMessage(level LogLevel, message string, a ...any) { 71 | if level >= l.level { 72 | l.logger.Output(3, l.formatMessage(level, message, a...)) 73 | } 74 | } 75 | 76 | // Logging methods. 77 | func (l *Logger) Info(message string, a ...any) { l.logMessage(Info, message, a...) } 78 | func (l *Logger) Debug(message string, a ...any) { l.logMessage(Debug, message, a...) } 79 | func (l *Logger) Warning(message string, a ...any) { l.logMessage(Warning, message, a...) } 80 | func (l *Logger) Error(message string, a ...any) { l.logMessage(Error, message, a...) } 81 | func (l *Logger) Fatal(message string, a ...any) { 82 | l.logMessage(Fatal, message, a...) 83 | os.Exit(1) 84 | } 85 | -------------------------------------------------------------------------------- /storage.go: -------------------------------------------------------------------------------- 1 | package goattck 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | "os" 8 | ) 9 | 10 | var ( 11 | DefaultFileName = "./enterprise-attack.json" 12 | ) 13 | 14 | type storage interface { 15 | ExistsLocally() (bool, error) 16 | DownloadAndSave(url string) (RawEnterpriseAttck, error) 17 | Download(url string) (RawEnterpriseAttck, error) 18 | Save(enterprise RawEnterpriseAttck) error 19 | Retrieve() (RawEnterpriseAttck, error) 20 | } 21 | 22 | var _ (storage) = new(Storage) 23 | 24 | type Storage struct { 25 | } 26 | 27 | func (s Storage) ExistsLocally() (bool, error) { 28 | if _, err := os.Stat(DefaultFileName); err == nil { 29 | return true, nil 30 | } else { 31 | return false, err 32 | } 33 | } 34 | 35 | // Downloads and stores json data to local disk 36 | func (s Storage) DownloadAndSave(url string) (RawEnterpriseAttck, error) { 37 | // If it doesn't exist locally, we download it 38 | raw, err := s.Download(url) 39 | if err != nil { 40 | return raw, err 41 | } 42 | err = s.Save(raw) 43 | if err != nil { 44 | return RawEnterpriseAttck{}, err 45 | } 46 | return raw, nil 47 | } 48 | 49 | // Downloads the enterprise-attack json from the provided URL 50 | func (s Storage) Download(url string) (RawEnterpriseAttck, error) { 51 | resp, err := http.Get(url) 52 | if err != nil { 53 | return RawEnterpriseAttck{}, err 54 | } 55 | defer resp.Body.Close() 56 | return s.unmarshall(resp.Body) 57 | } 58 | 59 | // Encodes and store the provided data object to the provided path 60 | func (s Storage) Save(enterprise RawEnterpriseAttck) error { 61 | file, err := os.Create(DefaultFileName) 62 | if err != nil { 63 | return err 64 | } 65 | defer file.Close() 66 | 67 | return json.NewEncoder(file).Encode(enterprise) 68 | } 69 | 70 | // Retrieves the RawEnterpriseAttck from local disk using the provided path 71 | func (s Storage) Retrieve() (RawEnterpriseAttck, error) { 72 | var raw RawEnterpriseAttck 73 | 74 | file, err := os.Open(DefaultFileName) 75 | if err != nil { 76 | return raw, err 77 | } 78 | defer file.Close() 79 | 80 | bytes, err := io.ReadAll(file) 81 | if err != nil { 82 | return raw, err 83 | } 84 | if err := json.Unmarshal(bytes, &raw); err != nil { 85 | return raw, err 86 | } 87 | return raw, nil 88 | } 89 | 90 | func (s Storage) unmarshall(data io.Reader) (RawEnterpriseAttck, error) { 91 | eAttck := RawEnterpriseAttck{} 92 | byteValue, err := io.ReadAll(data) 93 | if err != nil { 94 | return eAttck, err 95 | } 96 | bytesData := []byte(byteValue) 97 | if err != nil { 98 | return eAttck, err 99 | } 100 | 101 | json.Unmarshal(bytesData, &eAttck) 102 | return eAttck, nil 103 | } 104 | -------------------------------------------------------------------------------- /internal/models/technique.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | ) 7 | 8 | type technique interface{} 9 | 10 | type Technique struct { 11 | BaseModel 12 | BaseAttributes 13 | // These are properties from the MITRE ATT&CK json 14 | XMitrePlatforms []string `json:"x_mitre_platforms"` 15 | ExternalReferences []ExternalReference `json:"external_references"` 16 | KillChainPhases []struct { 17 | KillChainName string `json:"kill_chain_name"` 18 | PhaseName string `json:"phase_name"` 19 | } `json:"kill_chain_phases"` 20 | XMitreDetection string `json:"x_mitre_detection,omitempty"` 21 | XMitreIsSubtechnique bool `json:"x_mitre_is_subtechnique"` 22 | XMitreModifiedByRef string `json:"x_mitre_modified_by_ref"` 23 | XMitreDataSources []string `json:"x_mitre_data_sources,omitempty"` 24 | XMitreDefenseBypassed []string `json:"x_mitre_defense_bypassed,omitempty"` 25 | XMitreContributors []string `json:"x_mitre_contributors,omitempty"` 26 | XMitrePermissionsRequired []string `json:"x_mitre_permissions_required,omitempty"` 27 | XMitreRemoteSupport bool `json:"x_mitre_remote_support,omitempty"` 28 | XMitreAttackSpecVersion string `json:"x_mitre_attack_spec_version,omitempty"` 29 | XMitreSystemRequirements []string `json:"x_mitre_system_requirements,omitempty"` 30 | XMitreImpactType []string `json:"x_mitre_impact_type,omitempty"` 31 | XMitreEffectivePermissions []string `json:"x_mitre_effective_permissions,omitempty"` 32 | XMitreNetworkRequirements bool `json:"x_mitre_network_requirements,omitempty"` 33 | techniqueExternalAttributes 34 | Actors []Actor 35 | Campaigns []Campaign 36 | DataComponents []DataComponent 37 | DataSources []DataSource 38 | Malwares []Malware 39 | Mitigations []Mitigation 40 | Tactics []Tactic 41 | Techniques []Technique 42 | Tools []Tool 43 | } 44 | 45 | var _ (technique) = new(Technique) 46 | 47 | type techniqueExternalAttributes struct { 48 | // These are properties external from the MITRE ATT&CK json definitions 49 | CommandList []string `json:"command_list"` 50 | Commands []string `json:"commands"` 51 | Queries []string `json:"queries"` 52 | ParsedDatasets []string `json:"parsed_datasets"` 53 | PossibleDetections []string `json:"possible_detections"` 54 | ExternalReference []string `json:"external_reference"` 55 | Controls []string `json:"controls"` 56 | TechniqueId string `json:"technique_id"` 57 | } 58 | 59 | func NewTechnique(object map[string]interface{}) (Technique, error) { 60 | technique := Technique{} 61 | jsonString, _ := json.Marshal(object) 62 | json.Unmarshal(jsonString, &technique) 63 | return technique, nil 64 | } 65 | 66 | func (t *Technique) GetExternalID() string { 67 | mitreID := "" 68 | for _, ref := range t.ExternalReferences { 69 | if ref.ExternalId != "" && mitreID == "" { 70 | if strings.HasPrefix(ref.ExternalId, "T") { 71 | mitreID = ref.ExternalId 72 | } 73 | } 74 | } 75 | return mitreID 76 | } 77 | -------------------------------------------------------------------------------- /tests/data/malware.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "malware--007b44b6-e4c5-480b-b5b9-56f2081b1b7b", 3 | "name": "HDoor", 4 | "created": "2017-05-31T21:32:40.801Z", 5 | "modified": "2023-04-04T20:20:59.961Z", 6 | "x_mitre_version": "1.0", 7 | "type": "malware", 8 | "labels": [ 9 | "malware" 10 | ], 11 | "description": "[HDoor](https://attack.mitre.org/software/S0061) is malware that has been customized and used by the [Naikon](https://attack.mitre.org/groups/G0019) group. (Citation: Baumgartner Naikon 2015)", 12 | "external_references": [ 13 | { 14 | "source_name": "mitre-attack", 15 | "url": "https://attack.mitre.org/software/S0061", 16 | "external_id": "S0061", 17 | "description": "" 18 | }, 19 | { 20 | "source_name": "Baumgartner Naikon 2015", 21 | "url": "https://media.kasperskycontenthub.com/wp-content/uploads/sites/43/2018/03/07205555/TheNaikonAPT-MsnMM1.pdf", 22 | "external_id": "", 23 | "description": "Baumgartner, K., Golovkin, M.. (2015, May). The MsnMM Campaigns: The Earliest Naikon APT Campaigns. Retrieved April 10, 2019." 24 | } 25 | ], 26 | "object_marking_refs": [ 27 | "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" 28 | ], 29 | "revoked": false, 30 | "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 31 | "x_mitre_deprecated": false, 32 | "x_mitre_contributors": [], 33 | "x_mitre_aliases": [ 34 | "HDoor", 35 | "Custom HDoor" 36 | ], 37 | "x_mitre_platforms": [ 38 | "Windows" 39 | ], 40 | "names": [], 41 | "comments": [], 42 | "family": [], 43 | "links": [], 44 | "license": "", 45 | "price": "", 46 | "github": "", 47 | "site": "", 48 | "twitter": "", 49 | "evaluator": "", 50 | "date": "", 51 | "version": 0, 52 | "implementation": "", 53 | "how_to": "", 54 | "slingshot": "", 55 | "kali": "", 56 | "server": "", 57 | "implant": "", 58 | "multi_user": false, 59 | "ui": false, 60 | "dark_mode": false, 61 | "api": false, 62 | "windows": false, 63 | "linux": false, 64 | "macos": false, 65 | "tcp": false, 66 | "http": false, 67 | "http2": false, 68 | "http3": false, 69 | "dns": false, 70 | "doh": false, 71 | "icmp": false, 72 | "ftp": false, 73 | "imap": false, 74 | "mapi": false, 75 | "smb": false, 76 | "ldap": false, 77 | "key_exchange": false, 78 | "stego": false, 79 | "proxy_aware": false, 80 | "domainfront": false, 81 | "custom_profile": false, 82 | "jitter": false, 83 | "working_hours": false, 84 | "kill_date": false, 85 | "chaining": false, 86 | "logging": false, 87 | "in_wild": false, 88 | "attck_mapping": false, 89 | "dashboard": false, 90 | "blog": "", 91 | "c2_matrix_indicators": "", 92 | "jarm": false, 93 | "actively_maint": false, 94 | "slack": false, 95 | "slack_members": false, 96 | "gh_issues": false, 97 | "notes": "", 98 | "socks_support": false, 99 | "x_mitre_old_attack_id": "", 100 | "x_mitre_attack_spec_version": "3.1.0", 101 | "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 102 | "x_mitre_domains": [ 103 | "enterprise-attack" 104 | ], 105 | "is_family": false 106 | } -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/msadministrator/goattck" 11 | 12 | "github.com/msadministrator/goattck/internal/logger" 13 | "github.com/msadministrator/goattck/internal/menu" 14 | ) 15 | 16 | var ( 17 | slogger = logger.NewLogger(logger.Debug) 18 | defaultAttckUrl = "https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json" 19 | attck goattck.Enterprise 20 | ) 21 | 22 | const logo = ` 23 | __ __ __ 24 | ____ _________ _/ |__/ |_ ____ | | __ 25 | / ___\ / _ \__ \\ __\ __\/ ___\| |/ / 26 | / /_/ > <_> ) __ \| | | | \ \___| < 27 | \___ / \____(____ /__| |__| \___ >__|_ \ 28 | /_____/ \/ \/ \/ 29 | ` 30 | 31 | // rootCmd represents the base command when called without any subcommands 32 | var rootCmd = &cobra.Command{ 33 | Use: "goattck", 34 | Short: "A golang CLI for the MITRE ATT&CK Framework", 35 | Long: ReturnLogo() + `This is a golang CLI for the MITRE ATT&CK Framework. 36 | It utilizes work from pyattck and the MITRE ATT&CK API`, 37 | } 38 | 39 | var enterpriseCmd = &cobra.Command{ 40 | Use: "enterprise", 41 | Short: "Sets the scope of the CLI tool to the Enterprise MITRE ATT&CK Franework.", 42 | Long: ReturnLogo() + "Sets the scope of the CLI tool to the Enterprise MITRE ATT&CK Franework.", 43 | Run: func(cmd *cobra.Command, args []string) { 44 | // Simple placeholder 45 | }, 46 | } 47 | 48 | var menuCmd = &cobra.Command{ 49 | Use: "menu", 50 | Short: "Displays an interactive UI for Mitre ATT&CK", 51 | Long: ReturnLogo() + "This launches the cool (mid-90s) interactive menu using frameworks like bubbletea and lipgloss to make it look all fancy", 52 | Run: func(cmd *cobra.Command, args []string) { 53 | if cmd.Parent().Name() == "enterprise" { 54 | 55 | enterprise, err := goattck.Enterprise{}.New(defaultAttckUrl) 56 | if err != nil { 57 | slogger.Fatal(fmt.Sprintf("Error loading MITRE ATT&CK data: %v\n", err)) 58 | } 59 | if len(args) > 0 { 60 | val, err := strconv.ParseBool(args[0]) 61 | if err != nil { 62 | slogger.Fatal("Unknown arguemnt value provided.") 63 | } 64 | if val { 65 | // if Force == true & we didn't just download it 66 | slogger.Debug(fmt.Sprintf("downloading and saving latest json data from %s", defaultAttckUrl)) 67 | enterprise, err = enterprise.Load(val) 68 | if err != nil { 69 | slogger.Fatal(fmt.Sprintf("unable to download from url: %s %s", defaultAttckUrl, err)) 70 | } 71 | } 72 | } 73 | 74 | attck, err = enterprise.Load(false) 75 | if err != nil { 76 | slogger.Fatal(fmt.Sprintf("Error loading MITRE ATT&CK data: %v\n", err)) 77 | } 78 | 79 | menu.Load(attck) 80 | } else { 81 | slogger.Warning(fmt.Sprintf("unknown parent %s use 'enterprise' as parent instead", cmd.Parent().Name())) 82 | } 83 | }, 84 | } 85 | 86 | func Execute() { 87 | var forceDownload bool 88 | rootCmd.PersistentFlags().BoolVar(&forceDownload, "force", false, "Force download latest Mitre ATT&CK JSON") 89 | enterpriseCmd.AddCommand(menuCmd) 90 | rootCmd.AddCommand(enterpriseCmd) 91 | err := rootCmd.Execute() 92 | if err != nil { 93 | os.Exit(1) 94 | } 95 | } 96 | 97 | func ReturnLogo() string { 98 | return fmt.Sprintf("\u200B %s", logo) 99 | } 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goattck 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/msadministrator/goattck.svg)](https://pkg.go.dev/github.com/msadministrator/goattck) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/msadministrator/goattck)](https://goreportcard.com/report/github.com/msadministrator/goattck) 5 | 6 | A comprehensive Go implementation for interacting with the MITRE ATT&CK Framework, available both as a CLI tool and a Go package. 7 | 8 | ## Features 9 | 10 | - **CLI Interface**: Interactive terminal-based interface for exploring MITRE ATT&CK data 11 | - **Go Package**: Programmatic access to ATT&CK data in your Go applications 12 | - **Enterprise Focus**: Comprehensive coverage of the Enterprise ATT&CK framework 13 | - **Data Management**: Automatic downloading and caching of ATT&CK data 14 | - **Modern UI**: Beautiful terminal interface using bubbletea and lipgloss 15 | 16 | ## Installation 17 | 18 | ### CLI Installation 19 | 20 | ```bash 21 | go install github.com/msadministrator/goattck@latest 22 | ``` 23 | 24 | ### Package Installation 25 | 26 | ```bash 27 | go get github.com/msadministrator/goattck 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### CLI Usage 33 | 34 | ![](./docs/technique-overview.png) 35 | 36 | When using the CLI, you must first specify the data you are wanting to access. Currently this is `enterprise` but there are future plans to extend this beyond just the enterprise framework. 37 | 38 | > The enterprise command does not currently do anything so just use the `menu` subcommand. 39 | 40 | ```bash 41 | goattck enterprise 42 | ``` 43 | 44 | To access the fancy interactive menu, you then must specify the `menu` command. Once you run this command you must select the entity and the specific data object to then display the more detailed view. 45 | 46 | ```bash 47 | goattck enterprise menu 48 | ``` 49 | 50 | By default, we first download the JSON data from [mitre](https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json) and store it locally. If you want to retrieve the latest version or update what is stored locally you must run the following command using the `--force` flag. 51 | 52 | ```bash 53 | goattck enterprise menu --force 54 | ``` 55 | 56 | ### Package Usage 57 | 58 | When wanting to integrate `goattck` into your project, you can 59 | 60 | ```go 61 | import ( 62 | "github.com/msadministrator/goattck" 63 | ) 64 | 65 | func main() { 66 | 67 | enterprise, err := goattck.Enterprise{}.New("https://raw.githubusercontent.com/mitre/cti/master/enterprise-attack/enterprise-attack.json") 68 | if err != nil { 69 | // Handle error 70 | } 71 | 72 | forceDownload := false 73 | enterprise, err = enterprise.Load(forceDownload) 74 | 75 | // Access ATT&CK data 76 | for _, technnique := range enterprise.Techniques { 77 | fmt.Printf("technique: %s", technique.Name) 78 | } 79 | } 80 | ``` 81 | 82 | ## Data Models 83 | 84 | The package provides comprehensive models for ATT&CK data: 85 | 86 | - `Actor`: Threat actor information 87 | - `Technique`: ATT&CK techniques 88 | - `Tactic`: Tactics used in the framework 89 | - `Malware`: Malware information 90 | - `Tool`: Tools used in attacks 91 | - `Campaign`: Campaign information 92 | - `Mitigation`: Mitigation strategies 93 | - `DataSource`: Data sources for detection 94 | - `DataComponent`: Components of data sources 95 | 96 | ## Contributing 97 | 98 | Contributions are welcome! Please feel free to submit a Pull Request. 99 | 100 | ## Acknowledgments 101 | 102 | - [MITRE ATT&CK](https://attack.mitre.org/) 103 | - [pyattck](https://github.com/swimlane/pyattck) 104 | - [bubbletea](https://github.com/charmbracelet/bubbletea) 105 | - [lipgloss](https://github.com/charmbracelet/lipgloss) 106 | -------------------------------------------------------------------------------- /tests/data/actor.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "intrusion-set--6a2e693f-24e5-451a-9f88-b36a108e5662", 3 | "name": "APT1", 4 | "created": "2017-05-31T21:31:47.955Z", 5 | "modified": "2021-05-26T12:23:48.842Z", 6 | "x_mitre_version": "1.4", 7 | "type": "intrusion-set", 8 | "aliases": [ 9 | "APT1", 10 | "Comment Crew", 11 | "Comment Group", 12 | "Comment Panda" 13 | ], 14 | "x_mitre_contributors": [], 15 | "revoked": false, 16 | "description": "[APT1](https://attack.mitre.org/groups/G0006) is a Chinese threat group that has been attributed to the 2nd Bureau of the People\u2019s Liberation Army (PLA) General Staff Department\u2019s (GSD) 3rd Department, commonly known by its Military Unit Cover Designator (MUCD) as Unit 61398. (Citation: Mandiant APT1)", 17 | "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 18 | "x_mitre_deprecated": false, 19 | "x_mitre_attack_spec_version": "", 20 | "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 21 | "x_mitre_domains": [ 22 | "enterprise-attack" 23 | ], 24 | "object_marking_refs": [ 25 | "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" 26 | ], 27 | "external_references": [ 28 | { 29 | "source_name": "mitre-attack", 30 | "url": "https://attack.mitre.org/groups/G0006", 31 | "external_id": "G0006", 32 | "description": "" 33 | }, 34 | { 35 | "source_name": "APT1", 36 | "url": "", 37 | "external_id": "", 38 | "description": "(Citation: Mandiant APT1)" 39 | }, 40 | { 41 | "source_name": "Comment Crew", 42 | "url": "", 43 | "external_id": "", 44 | "description": "(Citation: Mandiant APT1)" 45 | }, 46 | { 47 | "source_name": "Comment Group", 48 | "url": "", 49 | "external_id": "", 50 | "description": "(Citation: Mandiant APT1)" 51 | }, 52 | { 53 | "source_name": "Comment Panda", 54 | "url": "", 55 | "external_id": "", 56 | "description": "(Citation: CrowdStrike Putter Panda)" 57 | }, 58 | { 59 | "source_name": "Mandiant APT1", 60 | "url": "https://www.fireeye.com/content/dam/fireeye-www/services/pdfs/mandiant-apt1-report.pdf", 61 | "external_id": "", 62 | "description": "Mandiant. (n.d.). APT1 Exposing One of China\u2019s Cyber Espionage Units. Retrieved July 18, 2016." 63 | }, 64 | { 65 | "source_name": "CrowdStrike Putter Panda", 66 | "url": "http://cdn0.vox-cdn.com/assets/4589853/crowdstrike-intelligence-report-putter-panda.original.pdf", 67 | "external_id": "", 68 | "description": "Crowdstrike Global Intelligence Team. (2014, June 9). CrowdStrike Intelligence Report: Putter Panda. Retrieved January 22, 2016." 69 | } 70 | ], 71 | "names": [ 72 | "Comment Crew", 73 | "Comment Panda", 74 | "PLA Unit 61398", 75 | "TG-8223", 76 | "APT1", 77 | "BrownFox", 78 | "GIF89a, ShadyRAT, Shanghai Group, Byzantine Candor", 79 | "G0006" 80 | ], 81 | "external_tools": [ 82 | "WEBC2", 83 | "BISCUIT and many others" 84 | ], 85 | "country": [], 86 | "operations": [], 87 | "links": [ 88 | "http://www.mcafee.com/us/resources/white-papers/wp-operation-shady-rat.pdf", 89 | "http://www.nytimes.com/2013/02/19/technology/chinas-army-is-seen-as-tied-to-hacking-against-us.html?emc=na&_r=2&", 90 | "https://www.secureworks.com/research/analysis-of-dhs-nccic-indicators", 91 | "https://www.scribd.com/doc/13731776/Tracking-GhostNet-Investigating-a-Cyber-Espionage-Network", 92 | "http://www.nartv.org/mirror/ghostnet.pdf" 93 | ], 94 | "targets": [ 95 | "U.S. cybersecurity firm Mandiant, later purchased by FireEye, released a report in February 2013 that exposed one of China's cyber espionage units, Unit 61398. The group, which FireEye called APT1, is a unit within China's People's Liberation Army (PLA) that has been linked to a wide range of cyber operations targeting U.S. private sector entities for espionage purposes. The comprehensive report detailed evidence connecting APT1 and the PLA, offered insight into APT1's operational malware and methodologies, and provided timelines of the espionage it conducted." 96 | ], 97 | "external_description": [], 98 | "attck_id": "", 99 | "attck_ids": [], 100 | "comment": "", 101 | "comments": [] 102 | } -------------------------------------------------------------------------------- /internal/menu/main.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | bubblelist "github.com/charmbracelet/bubbles/list" 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/charmbracelet/lipgloss" 10 | 11 | "github.com/msadministrator/goattck" 12 | ) 13 | 14 | const ( 15 | // Just setting this to a standard width for now. 16 | width = 120 17 | ) 18 | 19 | // Style definitions. 20 | // Most of this was borrowed from their example 21 | // https://github.com/charmbracelet/lipgloss/blob/master/examples/layout/main.go 22 | var ( 23 | 24 | // General. 25 | normal = lipgloss.Color("#EEEEEE") 26 | subtle = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} 27 | highlight = lipgloss.AdaptiveColor{Light: "#874BFD", Dark: "#7D56F4"} 28 | special = lipgloss.AdaptiveColor{Light: "#43BF6D", Dark: "#73F59F"} 29 | 30 | base = lipgloss.NewStyle().Foreground(normal) 31 | 32 | divider = lipgloss.NewStyle(). 33 | SetString("•"). 34 | Padding(0, 1). 35 | Foreground(subtle). 36 | String() 37 | 38 | url = lipgloss.NewStyle().Foreground(special).Render 39 | 40 | // Tabs. 41 | activeTabBorder = lipgloss.Border{ 42 | Top: "─", 43 | Bottom: " ", 44 | Left: "│", 45 | Right: "│", 46 | TopLeft: "╭", 47 | TopRight: "╮", 48 | BottomLeft: "┘", 49 | BottomRight: "└", 50 | } 51 | 52 | tabBorder = lipgloss.Border{ 53 | Top: "─", 54 | Bottom: "─", 55 | Left: "│", 56 | Right: "│", 57 | TopLeft: "╭", 58 | TopRight: "╮", 59 | BottomLeft: "┴", 60 | BottomRight: "┴", 61 | } 62 | 63 | tab = lipgloss.NewStyle(). 64 | Border(tabBorder, true). 65 | BorderForeground(highlight). 66 | Padding(0, 1) 67 | 68 | activeTab = tab.Border(activeTabBorder, true) 69 | 70 | tabGap = tab. 71 | BorderTop(false). 72 | BorderLeft(false). 73 | BorderRight(false) 74 | 75 | // Title. 76 | titleStyle = lipgloss.NewStyle(). 77 | MarginLeft(1). 78 | MarginRight(5). 79 | Padding(0, 1). 80 | Italic(true). 81 | Foreground(lipgloss.Color("#FFF7DB")) 82 | 83 | descStyle = base.MarginTop(1) 84 | 85 | infoStyle = base. 86 | BorderStyle(lipgloss.NormalBorder()). 87 | BorderTop(true). 88 | BorderForeground(subtle) 89 | 90 | // description. 91 | descriptionStyle = lipgloss.NewStyle(). 92 | Align(lipgloss.Center). 93 | Foreground(lipgloss.Color("#FAFAFA")). 94 | Background(highlight). 95 | Margin(1, 3, 0, 0). 96 | Padding(1, 2) 97 | 98 | // Status Bar. 99 | statusNugget = lipgloss.NewStyle(). 100 | Foreground(lipgloss.Color("#FFFDF5")). 101 | Padding(0, 1) 102 | 103 | statusBarStyle = lipgloss.NewStyle(). 104 | Foreground(lipgloss.AdaptiveColor{Light: "#343433", Dark: "#C1C6B2"}). 105 | Background(lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#353533"}) 106 | 107 | statusStyle = lipgloss.NewStyle(). 108 | Inherit(statusBarStyle). 109 | Foreground(lipgloss.Color("#FFFDF5")). 110 | Background(lipgloss.Color("#FF5F87")). 111 | Padding(0, 1). 112 | MarginRight(1) 113 | 114 | encodingStyle = statusNugget. 115 | Background(lipgloss.Color("#A550DF")). 116 | Align(lipgloss.Right) 117 | 118 | statusText = lipgloss.NewStyle().Inherit(statusBarStyle) 119 | 120 | // Page. 121 | docStyle = lipgloss.NewStyle().Padding(1, 2, 1, 2) 122 | ) 123 | 124 | // The main function which loads our attck data model 125 | func Load(attck goattck.Enterprise) { 126 | items := []bubblelist.Item{ 127 | item{title: "actors", desc: "View threat actors and groups"}, 128 | item{title: "campaigns", desc: "View threat campaigns"}, 129 | item{title: "controls", desc: "View security controls"}, 130 | item{title: "data sources", desc: "View data sources"}, 131 | item{title: "malwares", desc: "View malware"}, 132 | item{title: "mitigations", desc: "View mitigations"}, 133 | item{title: "tactics", desc: "View tactics"}, 134 | item{title: "techniques", desc: "View techniques"}, 135 | item{title: "tools", desc: "View tools"}, 136 | } 137 | 138 | m := model{ 139 | list: bubblelist.New(items, bubblelist.NewDefaultDelegate(), 0, 0), 140 | tabs: mitreTabs, 141 | enterprise: attck, 142 | state: stateSelectEntity, 143 | } 144 | m.list.Title = "MITRE ATT&CK Enterprise Framework" 145 | m.list.SetShowStatusBar(false) 146 | m.list.SetFilteringEnabled(true) 147 | m.list.Styles.Title = titleStyle 148 | 149 | p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion()) 150 | 151 | if _, err := p.Run(); err != nil { 152 | fmt.Println("Error running program:", err) 153 | os.Exit(1) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /internal/models/malware.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | ) 7 | 8 | type malware interface{} 9 | 10 | type Malware struct { 11 | BaseModel 12 | BaseAttributes 13 | // These are properties from the MITRE ATT&CK json 14 | XMitrePlatforms []string `json:"x_mitre_platforms,omitempty"` 15 | XMitreAliases []string `json:"x_mitre_aliases,omitempty"` 16 | ExternalReferences []ExternalReference `json:"external_references"` 17 | Labels []string `json:"labels"` 18 | XMitreAttackSpecVersion string `json:"x_mitre_attack_spec_version"` 19 | XMitreModifiedByRef string `json:"x_mitre_modified_by_ref"` 20 | XMitreContributors []string `json:"x_mitre_contributors,omitempty"` 21 | malwareExternalAttributes 22 | Actors []Actor 23 | Campaigns []Campaign 24 | Techniques []Technique 25 | } 26 | 27 | var _ (malware) = new(Malware) 28 | 29 | type malwareExternalAttributes struct { 30 | // external collected data attributes below 31 | Names []string `json:"names"` 32 | Comments []string `json:"comments"` 33 | Family []string `json:"family"` 34 | Links []string `json:"links"` 35 | License string `json:"license"` 36 | Price string `json:"price"` 37 | Github string `json:"github"` 38 | Site string `json:"site"` 39 | Twitter string `json:"twitter"` 40 | Evaluator string `json:"evaluator"` 41 | Date string `json:"date"` 42 | Version string `json:"version"` 43 | Implementation string `json:"implementation"` 44 | HowTo string `json:"how_to"` 45 | Slingshot string `json:"slingshot"` 46 | Kali string `json:"kali"` 47 | Server string `json:"server"` 48 | Implant string `json:"implant"` 49 | MultiUser bool `json:"multi_user"` 50 | UI interface{} `json:"ui"` 51 | DarkMode interface{} `json:"dark_mode"` 52 | API bool `json:"api"` 53 | Windows bool `json:"windows"` 54 | Linux bool `json:"linux"` 55 | MacOS bool `json:"macos"` 56 | TCP bool `json:"tcp"` 57 | HTTP bool `json:"http"` 58 | HTTP2 bool `json:"http2"` 59 | HTTP3 bool `json:"http3"` 60 | DNS bool `json:"dns"` 61 | DOH bool `json:"doh"` 62 | ICMP bool `json:"icmp"` 63 | FTP bool `json:"ftp"` 64 | IMAP bool `json:"imap"` 65 | MAPI bool `json:"mapi"` 66 | SMB bool `json:"smb"` 67 | LDAP bool `json:"ldap"` 68 | KeyExchange bool `json:"key_exchange"` 69 | Stego bool `json:"stego"` 70 | ProxyAware bool `json:"proxy_aware"` 71 | DomainFront bool `json:"domainfront"` 72 | CustomProfile bool `json:"custom_profile"` 73 | Jitter bool `json:"jitter"` 74 | WorkingHours bool `json:"working_hours"` 75 | KillDate bool `json:"kill_date"` 76 | Chaining bool `json:"chaining"` 77 | Logging bool `json:"logging"` 78 | InWild bool `json:"in_wild"` 79 | AttckMapping bool `json:"attck_mapping"` 80 | Dashboard bool `json:"dashboard"` 81 | Blog string `json:"blog"` 82 | C2MatrixIndicators string `json:"c2_matrix_indicators"` 83 | Jarm bool `json:"jarm"` 84 | ActivelyMaint bool `json:"actively_maint"` 85 | Slack bool `json:"slack"` 86 | SlackMembers bool `json:"slack_members"` 87 | GhIssues bool `json:"gh_issues"` 88 | Notes string `json:"notes"` 89 | SocksSupport bool `json:"socks_support"` 90 | } 91 | 92 | func NewMalware(object map[string]interface{}) (Malware, error) { 93 | malware := Malware{} 94 | jsonString, _ := json.Marshal(object) 95 | json.Unmarshal(jsonString, &malware) 96 | return malware, nil 97 | } 98 | 99 | func (m *Malware) GetExternalID() string { 100 | mitreID := "" 101 | for _, ref := range m.ExternalReferences { 102 | if ref.ExternalId != "" && mitreID == "" { 103 | if strings.HasPrefix(ref.ExternalId, "S") { 104 | mitreID = ref.ExternalId 105 | } 106 | } 107 | } 108 | return mitreID 109 | } 110 | -------------------------------------------------------------------------------- /internal/models/tool.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | ) 7 | 8 | type tool interface{} 9 | 10 | type Tool struct { 11 | BaseModel 12 | BaseAttributes 13 | XMitrePlatforms []string `json:"x_mitre_platforms,omitempty"` 14 | XMitreDomains []string `json:"x_mitre_domains,omitempty"` 15 | XMitreContributors []string `json:"x_mitre_contributors,omitempty"` 16 | XMitreAliases []string `json:"x_mitre_aliases,omitempty"` 17 | ExternalReferences []ExternalReference `json:"external_references"` 18 | Labels []string `json:"labels"` 19 | XMitreAttackSpecVersion string `json:"x_mitre_attack_spec_version"` 20 | XMitreModifiedByRef string `json:"x_mitre_modified_by_ref"` 21 | // Additional [] of related entities 22 | Actors []Actor 23 | Campaigns []Campaign 24 | Techniques []Technique 25 | // External data 26 | C2Data interface{} `json:"c2_data"` 27 | ExternalDataset []interface{} `json:"external_dataset"` 28 | AdditionalNames []string `json:"additional_names"` 29 | AttributionLinks []string `json:"attribution_links"` 30 | AdditionalComments []string `json:"additional_comments"` 31 | Names []string `json:"names"` 32 | Comments []string `json:"comments"` 33 | Family []string `json:"family"` 34 | Links []string `json:"links"` 35 | License string `json:"license"` 36 | Price string `json:"price"` 37 | Github string `json:"github"` 38 | Site string `json:"site"` 39 | Twitter string `json:"twitter"` 40 | Evaluator string `json:"evaluator"` 41 | Date string `json:"date"` 42 | Version string `json:"version"` 43 | Implementation string `json:"implementation"` 44 | HowTo string `json:"how_to"` 45 | Slingshot string `json:"slingshot"` 46 | Kali string `json:"kali"` 47 | Server string `json:"server"` 48 | Implant string `json:"implant"` 49 | MultiUser bool `json:"multi_user"` 50 | UI bool `json:"ui"` 51 | DarkMode bool `json:"dark_mode"` 52 | Api bool `json:"api"` 53 | Windows bool `json:"windows"` 54 | Linux bool `json:"linux"` 55 | Macos bool `json:"macos"` 56 | Tcp bool `json:"tcp"` 57 | Http bool `json:"http"` 58 | Http2 bool `json:"http2"` 59 | Http3 bool `json:"http3"` 60 | Dns bool `json:"dns"` 61 | Doh bool `json:"doh"` 62 | Icmp bool `json:"icmp"` 63 | Ftp bool `json:"ftp"` 64 | Imap bool `json:"imap"` 65 | Mapi bool `json:"mapi"` 66 | SMB bool `json:"smb"` 67 | Ldap bool `json:"ldap"` 68 | KeyExchange bool `json:"key_exchange"` 69 | Stego bool `json:"stego"` 70 | ProxyAware bool `json:"proxy_aware"` 71 | Domainfront bool `json:"domainfront"` 72 | CustomProfile bool `json:"custom_profile"` 73 | Jitter bool `json:"jitter"` 74 | WorkingHours bool `json:"working_hours"` 75 | KillDate bool `json:"kill_date"` 76 | Chaining bool `json:"chaining"` 77 | Logging bool `json:"logging"` 78 | InWild bool `json:"in_wild"` 79 | AttckMapping bool `json:"attck_mapping"` 80 | Dashboard bool `json:"dashboard"` 81 | Blog string `json:"blog"` 82 | C2MatrixIndicators string `json:"c2_matrix_indicators"` 83 | Jarm bool `json:"jarm"` 84 | ActivelyMaint bool `json:"actively_maint"` 85 | Slack bool `json:"slack"` 86 | SlackMembers bool `json:"slack_members"` 87 | GhIssues bool `json:"gh_issues"` 88 | Notes string `json:"notes"` 89 | SocksSupport bool `json:"socks_support"` 90 | XMitreOldAttackId string `json:"x_mitre_old_attack_id"` 91 | } 92 | 93 | var _ (tool) = new(Tool) 94 | 95 | func NewTool(object map[string]interface{}) (Tool, error) { 96 | tool := Tool{} 97 | jsonString, _ := json.Marshal(object) 98 | json.Unmarshal(jsonString, &tool) 99 | return tool, nil 100 | } 101 | 102 | func (t *Tool) GetExternalID() string { 103 | mitreID := "" 104 | for _, ref := range t.ExternalReferences { 105 | if ref.ExternalId != "" && mitreID == "" { 106 | if strings.HasPrefix(ref.ExternalId, "S") { 107 | mitreID = ref.ExternalId 108 | } 109 | } 110 | } 111 | return mitreID 112 | } 113 | -------------------------------------------------------------------------------- /internal/models/base.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type entityStringValue string 8 | type entityStringArrayValue []string 9 | 10 | type BaseModel struct { 11 | Id string `json:"id"` 12 | Name string `json:"name"` 13 | Created string `json:"created"` 14 | Modified string `json:"modified"` 15 | XMitreVersion string `json:"x_mitre_version"` 16 | XMitreDomains []string `json:"x_mitre_domains"` 17 | Type string `json:"type"` 18 | } 19 | 20 | type BaseAttributes struct { 21 | Description string `json:"description"` 22 | XMitreDeprecated bool `json:"x_mitre_deprecated"` 23 | CreatedByRef string `json:"created_by_ref"` 24 | Revoked bool `json:"revoked"` 25 | ObjectMarkingRefs []string `json:"object_marking_refs"` 26 | } 27 | 28 | type BaseExternalModel struct { 29 | Aliases []string `json:"aliases"` 30 | ExternalReferences []ExternalReference `json:"external_references"` 31 | XMitreAttackSpecVersion string `json:"x_mitre_attack_spec_version"` 32 | XMitreModifiedByRef string `json:"x_mitre_modified_by_ref"` 33 | } 34 | 35 | type ExternalReference struct { 36 | SourceName string `json:"source_name"` 37 | Url string `json:"url"` 38 | ExternalId string `json:"external_id"` 39 | Description string `json:"description"` 40 | } 41 | 42 | func parseBaseAttributes(object map[string]interface{}) (BaseAttributes, error) { 43 | baseAttributes := BaseAttributes{} 44 | if object["description"] != nil { 45 | baseAttributes.Description = object["description"].(string) 46 | } 47 | if object["x_mitre_deprecated"] != nil { 48 | baseAttributes.XMitreDeprecated = object["x_mitre_deprecated"].(bool) 49 | } 50 | if object["created_by_ref"] != nil { 51 | baseAttributes.CreatedByRef = object["created_by_ref"].(string) 52 | } 53 | if object["revoked"] != nil { 54 | baseAttributes.Revoked = object["revoked"].(bool) 55 | } 56 | if object["object_marking_refs"] != nil { 57 | baseAttributes.ObjectMarkingRefs = ConvertInterfaceArrayToStringArray(object["object_marking_refs"].([]interface{})) 58 | } 59 | return baseAttributes, nil 60 | } 61 | 62 | func parseExternalReference(object map[string]interface{}) (ExternalReference, error) { 63 | externalReference := ExternalReference{} 64 | if object["source_name"] != nil { 65 | externalReference.SourceName = object["source_name"].(string) 66 | } 67 | if object["url"] != nil { 68 | externalReference.Url = object["url"].(string) 69 | } 70 | if object["external_id"] != nil { 71 | externalReference.ExternalId = object["external_id"].(string) 72 | } 73 | if object["description"] != nil { 74 | externalReference.Description = object["description"].(string) 75 | } 76 | return externalReference, nil 77 | } 78 | 79 | func parseExternalReferences(object map[string]interface{}) ([]ExternalReference, error) { 80 | extRefs := object["external_references"].([]interface{}) 81 | externalReferences := make([]ExternalReference, len(extRefs)) 82 | for i, v := range extRefs { 83 | ref, err := parseExternalReference(v.(map[string]interface{})) 84 | if err != nil { 85 | slogger.Error(fmt.Sprintf("Error parsing external references: %s", err)) 86 | } 87 | externalReferences[i] = ref 88 | } 89 | return externalReferences, nil 90 | } 91 | 92 | func parseExternalModel(object map[string]interface{}) (BaseExternalModel, error) { 93 | baseExternalModel := BaseExternalModel{} 94 | if object["aliases"] != nil { 95 | baseExternalModel.Aliases = ConvertInterfaceArrayToStringArray(object["aliases"].([]interface{})) 96 | } 97 | if object["external_references"] != nil { 98 | refs, err := parseExternalReferences(object) 99 | if err != nil { 100 | slogger.Error(fmt.Sprintf("Error parsing external references: %s", err)) 101 | } 102 | baseExternalModel.ExternalReferences = refs 103 | } 104 | if object["x_mitre_attack_spec_version"] != nil { 105 | baseExternalModel.XMitreAttackSpecVersion = object["x_mitre_attack_spec_version"].(string) 106 | } 107 | if object["x_mitre_modified_by_ref"] != nil { 108 | baseExternalModel.XMitreModifiedByRef = object["x_mitre_modified_by_ref"].(string) 109 | } 110 | return baseExternalModel, nil 111 | } 112 | 113 | func parseBaseModel(object map[string]interface{}) (BaseModel, error) { 114 | baseModel := BaseModel{} 115 | if object["id"] != nil { 116 | baseModel.Id = object["id"].(string) 117 | } 118 | if object["name"] != nil { 119 | baseModel.Name = object["name"].(string) 120 | } 121 | if object["created"] != nil { 122 | baseModel.Created = object["created"].(string) 123 | } 124 | if object["modified"] != nil { 125 | baseModel.Modified = object["modified"].(string) 126 | } 127 | if object["x_mitre_version"] != nil { 128 | baseModel.XMitreVersion = object["x_mitre_version"].(string) 129 | } 130 | if object["type"] != nil { 131 | baseModel.Type = object["type"].(string) 132 | } 133 | if object["x_mitre_domains"] != nil { 134 | baseModel.XMitreDomains = ConvertInterfaceArrayToStringArray(object["x_mitre_domains"].([]interface{})) 135 | } 136 | return baseModel, nil 137 | } 138 | -------------------------------------------------------------------------------- /tests/data/tool.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "tool--3433a9e8-1c47-4320-b9bf-ed449061d1c3", 3 | "name": "Empire", 4 | "created": "2019-03-11T14:13:40.648Z", 5 | "modified": "2023-03-22T03:43:09.336Z", 6 | "x_mitre_version": "1.6", 7 | "type": "tool", 8 | "description": "[Empire](https://attack.mitre.org/software/S0363) is an open source, cross-platform remote administration and post-exploitation framework that is publicly available on GitHub. While the tool itself is primarily written in Python, the post-exploitation agents are written in pure [PowerShell](https://attack.mitre.org/techniques/T1059/001) for Windows and Python for Linux/macOS. [Empire](https://attack.mitre.org/software/S0363) was one of five tools singled out by a joint report on public hacking tools being widely used by adversaries.(Citation: NCSC Joint Report Public Tools)(Citation: Github PowerShell Empire)(Citation: GitHub ATTACK Empire)", 9 | "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 10 | "labels": [ 11 | "tool" 12 | ], 13 | "x_mitre_platforms": [ 14 | "Linux", 15 | "macOS", 16 | "Windows" 17 | ], 18 | "x_mitre_aliases": [ 19 | "Empire", 20 | "EmPyre", 21 | "PowerShell Empire" 22 | ], 23 | "x_mitre_contributors": [], 24 | "external_references": [ 25 | { 26 | "source_name": "mitre-attack", 27 | "url": "https://attack.mitre.org/software/S0363", 28 | "external_id": "S0363", 29 | "description": "" 30 | }, 31 | { 32 | "source_name": "EmPyre", 33 | "url": "", 34 | "external_id": "", 35 | "description": "(Citation: Github PowerShell Empire)" 36 | }, 37 | { 38 | "source_name": "PowerShell Empire", 39 | "url": "", 40 | "external_id": "", 41 | "description": "(Citation: Github PowerShell Empire)" 42 | }, 43 | { 44 | "source_name": "Github PowerShell Empire", 45 | "url": "https://github.com/PowerShellEmpire/Empire", 46 | "external_id": "", 47 | "description": "Schroeder, W., Warner, J., Nelson, M. (n.d.). Github PowerShellEmpire. Retrieved April 28, 2016." 48 | }, 49 | { 50 | "source_name": "GitHub ATTACK Empire", 51 | "url": "https://github.com/dstepanic/attck_empire", 52 | "external_id": "", 53 | "description": "Stepanic, D. (2018, September 2). attck_empire: Generate ATT&CK Navigator layer file from PowerShell Empire agent logs. Retrieved March 11, 2019." 54 | }, 55 | { 56 | "source_name": "NCSC Joint Report Public Tools", 57 | "url": "https://www.ncsc.gov.uk/report/joint-report-on-publicly-available-hacking-tools", 58 | "external_id": "", 59 | "description": "The Australian Cyber Security Centre (ACSC), the Canadian Centre for Cyber Security (CCCS), the New Zealand National Cyber Security Centre (NZ NCSC), CERT New Zealand, the UK National Cyber Security Centre (UK NCSC) and the US National Cybersecurity and Communications Integration Center (NCCIC). (2018, October 11). Joint report on publicly available hacking tools. Retrieved March 11, 2019." 60 | } 61 | ], 62 | "object_marking_refs": [ 63 | "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" 64 | ], 65 | "revoked": false, 66 | "x_mitre_deprecated": false, 67 | "c2_data": {}, 68 | "external_dataset": [], 69 | "additional_names": [], 70 | "attribution_links": [], 71 | "additional_comments": [], 72 | "names": [], 73 | "comments": [], 74 | "family": [], 75 | "links": [], 76 | "license": "BSD3", 77 | "price": "NA", 78 | "github": "https://github.com/BC-SECURITY/Empire", 79 | "site": "", 80 | "twitter": "@BCSecurity1", 81 | "evaluator": "@jorgeorchilles", 82 | "date": "1/30/2020", 83 | "version": "3.0.5", 84 | "implementation": "install.sh", 85 | "how_to": "", 86 | "slingshot": "Yes", 87 | "kali": "Yes", 88 | "server": "Python", 89 | "implant": "PowerShell", 90 | "multi_user": false, 91 | "ui": "GUI", 92 | "dark_mode": false, 93 | "api": "Yes", 94 | "windows": "Yes", 95 | "linux": "Yes", 96 | "macos": "Yes", 97 | "tcp": "No", 98 | "http": "Yes", 99 | "http2": "No", 100 | "http3": "No", 101 | "dns": "No", 102 | "doh": "No", 103 | "icmp": "No", 104 | "ftp": "No", 105 | "imap": "No", 106 | "mapi": "No", 107 | "smb": "No", 108 | "ldap": "", 109 | "key_exchange": false, 110 | "stego": "No", 111 | "proxy_aware": false, 112 | "domainfront": "Yes", 113 | "custom_profile": false, 114 | "jitter": "Yes", 115 | "working_hours": false, 116 | "kill_date": false, 117 | "chaining": "No", 118 | "logging": "Yes", 119 | "in_wild": false, 120 | "attck_mapping": false, 121 | "dashboard": "No", 122 | "blog": "Yes", 123 | "c2_matrix_indicators": "", 124 | "jarm": "0ad0ad0000ad0ad22c42d42d000000088658245da669bb571fc2a62dd80912", 125 | "actively_maint": false, 126 | "slack": "#psempire bloodhoundhq.slack.com", 127 | "slack_members": false, 128 | "gh_issues": false, 129 | "notes": "Dropbox, OneDrive", 130 | "socks_support": false, 131 | "x_mitre_old_attack_id": "", 132 | "x_mitre_attack_spec_version": "3.1.0", 133 | "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 134 | "x_mitre_domains": [ 135 | "enterprise-attack" 136 | ] 137 | } -------------------------------------------------------------------------------- /internal/models/models_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewCampaign(t *testing.T) { 8 | campaign, err := NewCampaign(loadTestJSON("campaign.example.json")) 9 | if err != nil { 10 | t.Errorf("Error, could not load Actor: %v", err) 11 | } 12 | if campaign.Name != "SolarWinds Compromise" { 13 | t.Errorf("Error, could not load Campaign data models: %v", err) 14 | } 15 | if campaign.ExternalReferences[0].SourceName != "mitre-attack" { 16 | t.Errorf("Error, could not load Campaign data models: %v", err) 17 | } 18 | } 19 | 20 | func TestDataComponent(t *testing.T) { 21 | dataComponent, err := NewDataComponent(loadTestJSON("data_component.example.json")) 22 | if err != nil { 23 | t.Errorf("Error, could not load Actor: %v", err) 24 | } 25 | if dataComponent.Name != "User Account Authentication" { 26 | t.Errorf("Error, could not load DataComponent data models: %v", err) 27 | } 28 | if dataComponent.XMitreAttackSpecVersion != "2.1.0" { 29 | t.Errorf("Error, could not load DataComponent data models: %v", err) 30 | } 31 | } 32 | 33 | func TestDataSource(t *testing.T) { 34 | dataSource, err := NewDataSource(loadTestJSON("data_source.example.json")) 35 | if err != nil { 36 | t.Errorf("Error, could not load Actor: %v", err) 37 | } 38 | if dataSource.Name != "User Account" { 39 | t.Errorf("Error, could not load DataSource data models: %v", err) 40 | } 41 | if dataSource.XMitreVersion != "1.1" { 42 | t.Errorf("Error, could not load DataSource data models: %v", err) 43 | } 44 | if len(dataSource.XMitrePlatforms) != 9 { 45 | t.Errorf("Error, could not load DataSource data models: %v", err) 46 | } 47 | } 48 | 49 | func TestNewMarkingDefinition(t *testing.T) { 50 | markingDefinition, err := NewMarkingDefinition(loadTestJSON("definition.example.json")) 51 | if err != nil { 52 | t.Errorf("Error, could not load Actor: %v", err) 53 | } 54 | if markingDefinition.DefinitionType != "statement" { 55 | t.Errorf("Error, could not load MarkingDefinition data models: %v", err) 56 | } 57 | } 58 | 59 | func TestNewMalware(t *testing.T) { 60 | malware, err := NewMalware(loadTestJSON("malware.example.json")) 61 | if err != nil { 62 | t.Errorf("Error, could not load Actor: %v", err) 63 | } 64 | if malware.Name != "HDoor" { 65 | t.Errorf("Error, could not load Malware data models: %v", err) 66 | } 67 | if malware.XMitreVersion != "1.0" { 68 | t.Errorf("Error, could not load Malware data models: %v", err) 69 | } 70 | if len(malware.XMitrePlatforms) != 1 { 71 | t.Errorf("Error, could not load Malware data models: %v", err) 72 | } 73 | if malware.Windows != false { 74 | t.Errorf("Error, could not load Malware data models: %v", err) 75 | } 76 | } 77 | 78 | func TestNewMatrix(t *testing.T) { 79 | matrix, err := NewMatrix(loadTestJSON("matrix.example.json")) 80 | if err != nil { 81 | t.Errorf("Error, could not load Actor: %v", err) 82 | } 83 | if matrix.Name != "Enterprise ATT&CK" { 84 | t.Errorf("Error, could not load Matrix data models: %v", err) 85 | } 86 | if len(matrix.TacticRefs) != 14 { 87 | t.Errorf("Error, could not load Matrix data models: %v", err) 88 | } 89 | if matrix.XMitreVersion != "1.0" { 90 | t.Errorf("Error, could not load Matrix data models: %v", err) 91 | } 92 | } 93 | 94 | func TestNewMitigation(t *testing.T) { 95 | mitigation, err := NewMitigation(loadTestJSON("mitigation.example.json")) 96 | if err != nil { 97 | t.Errorf("Error, could not load Actor: %v", err) 98 | } 99 | if mitigation.Name != "Data from Information Repositories Mitigation" { 100 | t.Errorf("Error, could not load Mitigation data models: %v", err) 101 | } 102 | if len(mitigation.ExternalReferences) != 1 { 103 | t.Errorf("Error, could not load Mitigation data models: %v", err) 104 | } 105 | if mitigation.XMitreVersion != "1.0" { 106 | t.Errorf("Error, could not load Mitigation data models: %v", err) 107 | } 108 | } 109 | 110 | func TestNewRelationship(t *testing.T) { 111 | relationship, err := NewRelationship(loadTestJSON("relationship.example.json")) 112 | if err != nil { 113 | t.Errorf("Error, could not load Actor: %v", err) 114 | } 115 | if relationship.Id != "relationship--74f14668-7111-4f96-a307-4aac00d91cf4" { 116 | t.Errorf("Error, could not load Relationship data models: %v", err) 117 | } 118 | if relationship.ExternalReferences[0].SourceName != "Carbon Black HotCroissant April 2020" { 119 | t.Errorf("Error, could not load Relationship data models: %v", err) 120 | } 121 | } 122 | 123 | func TestNewTactic(t *testing.T) { 124 | tactic, err := NewTactic(loadTestJSON("tactic.example.json")) 125 | if err != nil { 126 | t.Errorf("Error, could not load Actor: %v", err) 127 | } 128 | if tactic.Name != "Credential Access" { 129 | t.Errorf("Error, could not load Tactic data models: %v", err) 130 | } 131 | if tactic.Modified != "2019-07-19T17:43:41.967Z" { 132 | t.Errorf("Error, could not load Tactic data models: %v", err) 133 | } 134 | if tactic.XMitreDomains[0] != "enterprise-attack" { 135 | t.Errorf("Error, could not load Tactic data models: %v", err) 136 | } 137 | } 138 | 139 | func TestNewTechnique(t *testing.T) { 140 | technique, err := NewTechnique(loadTestJSON("technique.example.json")) 141 | if err != nil { 142 | t.Errorf("Error, could not load Actor: %v", err) 143 | } 144 | if technique.Name != "Scheduled Task" { 145 | t.Errorf("Error, could not load Technique data models: %v", err) 146 | } 147 | if len(technique.XMitreDataSources) != 5 { 148 | t.Errorf("Error, could not load Technique data models: %v", err) 149 | } 150 | if technique.KillChainPhases[0].PhaseName != "execution" { 151 | t.Errorf("Error, could not load Technique data models: %v", err) 152 | } 153 | if len(technique.KillChainPhases) != 3 { 154 | t.Errorf("Error, could not load Technique data models: %v", err) 155 | } 156 | if technique.TechniqueId != "T1053.005" { 157 | t.Errorf("Error, could not load Technique data models: %v", err) 158 | } 159 | } 160 | 161 | func TestNewTool(t *testing.T) { 162 | tool, err := NewTool(loadTestJSON("tool.example.json")) 163 | if err != nil { 164 | t.Errorf("Error, could not load Actor: %v", err) 165 | } 166 | if tool.Name != "Empire" { 167 | t.Errorf("Error, could not load Tool data models: %v", err) 168 | } 169 | if tool.XMitreVersion != "1.6" { 170 | t.Errorf("Error, could not load Tool data models: %v", err) 171 | } 172 | if tool.XMitrePlatforms[0] != "Linux" { 173 | t.Errorf("Error, could not load Tool data models: %v", err) 174 | } 175 | if tool.ExternalReferences[5].SourceName != "NCSC Joint Report Public Tools" { 176 | t.Errorf("Error, could not load Tool data models: %v", err) 177 | } 178 | if tool.Server != "Python" { 179 | t.Errorf("Error, could not load Tool data models: %v", err) 180 | } 181 | if tool.MultiUser != false { 182 | t.Errorf("Error, could not load Tool data models: %v", err) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 2 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 4 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 5 | github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 6 | github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 7 | github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs= 8 | github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg= 9 | github.com/charmbracelet/bubbletea v1.3.4 h1:kCg7B+jSCFPLYRA52SDZjr51kG/fMUEoPoZrkaDHyoI= 10 | github.com/charmbracelet/bubbletea v1.3.4/go.mod h1:dtcUCyCGEX3g9tosuYiut3MXgY/Jsv9nKVdibKKRRXo= 11 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= 12 | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= 13 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 14 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 15 | github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 16 | github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 17 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= 18 | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 19 | github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= 20 | github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 21 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 22 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 23 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= 27 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= 28 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 29 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 30 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 31 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 32 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 33 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 34 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 35 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 36 | github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= 37 | github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= 38 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 39 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 40 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= 41 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= 42 | github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= 43 | github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= 44 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 45 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 46 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 47 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 48 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 49 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 50 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 51 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 52 | github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= 53 | github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 54 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 55 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 56 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 57 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 58 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 59 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 60 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 61 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 62 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 63 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 64 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 65 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 66 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 67 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 68 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 69 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 70 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 71 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 72 | golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= 73 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 74 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 75 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 76 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 77 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 78 | -------------------------------------------------------------------------------- /tests/data/campaign.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "campaign--808d6b30-df4e-4341-8248-724da4bac650", 3 | "name": "SolarWinds Compromise", 4 | "created": "2023-03-24T14:59:26.744Z", 5 | "modified": "2023-04-14T00:41:06.231Z", 6 | "x_mitre_version": "1.0", 7 | "type": "campaign", 8 | "description": "The [SolarWinds Compromise](https://attack.mitre.org/campaigns/C0024) was a sophisticated supply chain cyber operation conducted by [APT29](https://attack.mitre.org/groups/G0016) that was discovered in mid-December 2020. [APT29](https://attack.mitre.org/groups/G0016) used customized malware to inject malicious code into the SolarWinds Orion software build process that was later distributed through a normal software update; they also used password spraying, token theft, API abuse, spear phishing, and other supply chain attacks to compromise user accounts and leverage their associated access. Victims of this campaign included government, consulting, technology, telecom, and other organizations in North America, Europe, Asia, and the Middle East. Industry reporting initially referred to the actors involved in this campaign as UNC2452, NOBELIUM, StellarParticle, Dark Halo, and SolarStorm.(Citation: SolarWinds Advisory Dec 2020)(Citation: SolarWinds Sunburst Sunspot Update January 2021)(Citation: FireEye SUNBURST Backdoor December 2020)(Citation: Volexity SolarWinds)(Citation: CrowdStrike StellarParticle January 2022)(Citation: Unit 42 SolarStorm December 2020)(Citation: Microsoft Analyzing Solorigate Dec 2020)(Citation: Microsoft Internal Solorigate Investigation Blog) \n\nIn April 2021, the US and UK governments attributed the [SolarWinds Compromise](https://attack.mitre.org/campaigns/C0024) to Russia's Foreign Intelligence Service (SVR); public statements included citations to [APT29](https://attack.mitre.org/groups/G0016), Cozy Bear, and The Dukes.(Citation: NSA Joint Advisory SVR SolarWinds April 2021)(Citation: UK NSCS Russia SolarWinds April 2021)(Citation: Mandiant UNC2452 APT29 April 2022) The US government assessed that of the approximately 18,000 affected public and private sector customers of Solar Winds\u2019 Orion product, a much smaller number were compromised by follow-on [APT29](https://attack.mitre.org/groups/G0016) activity on their systems.(Citation: USG Joint Statement SolarWinds January 2021) ", 9 | "first_seen": "2019-08-01T05:00:00.000Z", 10 | "last_seen": "2021-01-01T06:00:00.000Z", 11 | "x_mitre_first_seen_citation": "(Citation: Unit 42 SolarStorm December 2020)", 12 | "x_mitre_last_seen_citation": "(Citation: MSTIC NOBELIUM May 2021)", 13 | "aliases": [ 14 | "SolarWinds Compromise" 15 | ], 16 | "x_mitre_deprecated": false, 17 | "x_mitre_contributors": [], 18 | "revoked": false, 19 | "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 20 | "external_references": [ 21 | { 22 | "source_name": "mitre-attack", 23 | "url": "https://attack.mitre.org/campaigns/C0024", 24 | "external_id": "C0024", 25 | "description": "" 26 | }, 27 | { 28 | "source_name": "Volexity SolarWinds", 29 | "url": "https://www.volexity.com/blog/2020/12/14/dark-halo-leverages-solarwinds-compromise-to-breach-organizations/", 30 | "external_id": "", 31 | "description": "Cash, D. et al. (2020, December 14). Dark Halo Leverages SolarWinds Compromise to Breach Organizations. Retrieved December 29, 2020." 32 | }, 33 | { 34 | "source_name": "CrowdStrike StellarParticle January 2022", 35 | "url": "https://www.crowdstrike.com/blog/observations-from-the-stellarparticle-campaign/", 36 | "external_id": "", 37 | "description": "CrowdStrike. (2022, January 27). Early Bird Catches the Wormhole: Observations from the StellarParticle Campaign. Retrieved February 7, 2022." 38 | }, 39 | { 40 | "source_name": "USG Joint Statement SolarWinds January 2021", 41 | "url": "https://www.cisa.gov/news-events/news/joint-statement-federal-bureau-investigation-fbi-cybersecurity-and-infrastructure", 42 | "external_id": "", 43 | "description": "FBI, CISA, ODNI, NSA. (2022, January 5). Joint Statement by the Federal Bureau of Investigation (FBI), the Cybersecurity and Infrastructure Security Agency (CISA), the Office of the Director of National Intelligence (ODNI), and the National Security Agency (NSA). Retrieved March 26, 2023." 44 | }, 45 | { 46 | "source_name": "FireEye SUNBURST Backdoor December 2020", 47 | "url": "https://www.fireeye.com/blog/threat-research/2020/12/evasive-attacker-leverages-solarwinds-supply-chain-compromises-with-sunburst-backdoor.html", 48 | "external_id": "", 49 | "description": "FireEye. (2020, December 13). Highly Evasive Attacker Leverages SolarWinds Supply Chain to Compromise Multiple Global Victims With SUNBURST Backdoor. Retrieved January 4, 2021." 50 | }, 51 | { 52 | "source_name": "Mandiant UNC2452 APT29 April 2022", 53 | "url": "https://www.mandiant.com/resources/blog/unc2452-merged-into-apt29", 54 | "external_id": "", 55 | "description": "Mandiant. (2020, April 27). Assembling the Russian Nesting Doll: UNC2452 Merged into APT29. Retrieved March 26, 2023." 56 | }, 57 | { 58 | "source_name": "MSTIC NOBELIUM May 2021", 59 | "url": "https://www.microsoft.com/security/blog/2021/05/27/new-sophisticated-email-based-attack-from-nobelium/", 60 | "external_id": "", 61 | "description": "Microsoft Threat Intelligence Center (MSTIC). (2021, May 27). New sophisticated email-based attack from NOBELIUM. Retrieved May 28, 2021." 62 | }, 63 | { 64 | "source_name": "Microsoft Internal Solorigate Investigation Blog", 65 | "url": "https://msrc-blog.microsoft.com/2021/02/18/microsoft-internal-solorigate-investigation-final-update/", 66 | "external_id": "", 67 | "description": "MSRC Team. (2021, February 18). Microsoft Internal Solorigate Investigation \u2013 Final Update. Retrieved May 14, 2021." 68 | }, 69 | { 70 | "source_name": "Microsoft Analyzing Solorigate Dec 2020", 71 | "url": "https://www.microsoft.com/security/blog/2020/12/18/analyzing-solorigate-the-compromised-dll-file-that-started-a-sophisticated-cyberattack-and-how-microsoft-defender-helps-protect/", 72 | "external_id": "", 73 | "description": "MSTIC. (2020, December 18). Analyzing Solorigate, the compromised DLL file that started a sophisticated cyberattack, and how Microsoft Defender helps protect customers . Retrieved January 5, 2021." 74 | }, 75 | { 76 | "source_name": "NSA Joint Advisory SVR SolarWinds April 2021", 77 | "url": "https://media.defense.gov/2021/Apr/15/2002621240/-1/-1/0/CSA_SVR_TARGETS_US_ALLIES_UOO13234021.PDF/CSA_SVR_TARGETS_US_ALLIES_UOO13234021.PDF", 78 | "external_id": "", 79 | "description": "NSA, FBI, DHS. (2021, April 15). Russian SVR Targets U.S. and Allied Networks. Retrieved April 16, 2021." 80 | }, 81 | { 82 | "source_name": "SolarWinds Advisory Dec 2020", 83 | "url": "https://www.solarwinds.com/sa-overview/securityadvisory", 84 | "external_id": "", 85 | "description": "SolarWinds. (2020, December 24). SolarWinds Security Advisory. Retrieved February 22, 2021." 86 | }, 87 | { 88 | "source_name": "SolarWinds Sunburst Sunspot Update January 2021", 89 | "url": "https://orangematter.solarwinds.com/2021/01/11/new-findings-from-our-investigation-of-sunburst/", 90 | "external_id": "", 91 | "description": "Sudhakar Ramakrishna . (2021, January 11). New Findings From Our Investigation of SUNBURST. Retrieved January 13, 2021." 92 | }, 93 | { 94 | "source_name": "UK NSCS Russia SolarWinds April 2021", 95 | "url": "https://www.ncsc.gov.uk/news/uk-and-us-call-out-russia-for-solarwinds-compromise", 96 | "external_id": "", 97 | "description": "UK NCSC. (2021, April 15). UK and US call out Russia for SolarWinds compromise. Retrieved April 16, 2021." 98 | }, 99 | { 100 | "source_name": "Unit 42 SolarStorm December 2020", 101 | "url": "https://unit42.paloaltonetworks.com/solarstorm-supply-chain-attack-timeline/", 102 | "external_id": "", 103 | "description": "Unit 42. (2020, December 23). SolarStorm Supply Chain Attack Timeline. Retrieved March 24, 2023." 104 | } 105 | ], 106 | "object_marking_refs": [ 107 | "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" 108 | ], 109 | "x_mitre_attack_spec_version": "3.1.0", 110 | "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", 111 | "x_mitre_domains": [ 112 | "enterprise-attack" 113 | ] 114 | } -------------------------------------------------------------------------------- /enterprise.go: -------------------------------------------------------------------------------- 1 | package goattck 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/msadministrator/goattck/internal/logger" 7 | "github.com/msadministrator/goattck/internal/models" 8 | ) 9 | 10 | var ( 11 | slogger = logger.NewLogger(logger.Info) 12 | ) 13 | 14 | // The raw representation of a custom data model used by both pyattck & goattck 15 | type RawEnterpriseAttck struct { 16 | // The type of framework 17 | AttckType string `json:"type"` 18 | // THe unique ID or hash of the JSON object 19 | ID string `json:"id"` 20 | // An array of data models structs for each entity 21 | Objects []interface{} `json:"objects"` 22 | // The defined version of the framework 23 | SpecVersion string `json:"spec_version"` 24 | // The last time the data was updated/modified. 25 | LastUpdated string `json:"last_updated"` 26 | // Whether or not this is a revoked version of the framework 27 | Revoked bool `json:"revoked"` 28 | } 29 | 30 | type enterprise interface { 31 | New(url string) (Enterprise, error) 32 | Load(force bool) (Enterprise, error) 33 | } 34 | 35 | // Enterprise struct represents the MITRE ATT&CK Enterprise framework 36 | type Enterprise struct { 37 | Actors []models.Actor 38 | Campaigns []models.Campaign 39 | Controls []models.Control 40 | DataComponents []models.DataComponent 41 | DataSources []models.DataSource 42 | Defintions []models.MarkingDefinition 43 | Malwares []models.Malware 44 | Matrices []models.Matrix 45 | Mitigations []models.Mitigation 46 | Relationships []models.Relationship 47 | Tactics []models.Tactic 48 | Techniques []models.Technique 49 | Tools []models.Tool 50 | rawData RawEnterpriseAttck 51 | attackRelationshipMap map[string][]string 52 | url string 53 | } 54 | 55 | var _ (enterprise) = new(Enterprise) 56 | 57 | // Checks if the data file exists locally and if not will download 58 | // and save the latest data. If it does exist, we use it. 59 | func (e Enterprise) New(url string) (Enterprise, error) { 60 | e.url = url 61 | s := Storage{} 62 | 63 | ok, err := s.ExistsLocally() 64 | if err != nil { 65 | slogger.Warning(fmt.Sprintf("error checking for file locally: %s", err)) 66 | } 67 | if !ok { 68 | // If it doesn't exist locally, we download it 69 | slogger.Debug(fmt.Sprintf("downloading and saving latest json data from %s", url)) 70 | e.rawData, err = s.DownloadAndSave(url) 71 | if err != nil { 72 | slogger.Fatal(fmt.Sprintf("unable to download from url: %s %s", url, err)) 73 | } 74 | } else { 75 | slogger.Debug("retrieving json from local disk") 76 | e.rawData, err = s.Retrieve() 77 | if err != nil { 78 | slogger.Fatal(fmt.Sprintf("error retrieving file from disk: %s", err)) 79 | } 80 | } 81 | 82 | return e, nil 83 | } 84 | 85 | // Loads our data models from our downloaded data 86 | func (e Enterprise) Load(force bool) (Enterprise, error) { 87 | err := e.loadDataModels() 88 | if err != nil { 89 | slogger.Error(fmt.Sprintf("Error loading data models: %s", err)) 90 | return e, err 91 | } 92 | return e, err 93 | } 94 | 95 | // Load data models from rawEnterpriseAttck struct 96 | func (e *Enterprise) loadDataModels() error { 97 | e.attackRelationshipMap = map[string][]string{} 98 | for _, value := range e.rawData.Objects { 99 | v, ok := value.(map[string]interface{}) 100 | if !ok { 101 | slogger.Error("error casting value to map") 102 | } 103 | switch v["type"] { 104 | case "intrusion-set": 105 | actor, err := models.NewActor(v) 106 | if err != nil { 107 | slogger.Error(fmt.Sprintf("Error creating actor: %s", err)) 108 | } 109 | e.Actors = append(e.Actors, actor) 110 | case "campaign": 111 | campaign, err := models.NewCampaign(v) 112 | if err != nil { 113 | slogger.Error(fmt.Sprintf("Error creating campaign: %s", err)) 114 | } 115 | e.Campaigns = append(e.Campaigns, campaign) 116 | case "x-mitre-data-component": 117 | dataComponent, err := models.NewDataComponent(v) 118 | if err != nil { 119 | slogger.Error(fmt.Sprintf("Error creating data component: %s", err)) 120 | } 121 | e.DataComponents = append(e.DataComponents, dataComponent) 122 | case "x-mitre-data-source": 123 | dataSource, err := models.NewDataSource(v) 124 | if err != nil { 125 | slogger.Error(fmt.Sprintf("Error creating data source: %s", err)) 126 | } 127 | e.DataSources = append(e.DataSources, dataSource) 128 | case "marking-definition": 129 | markingDefinition, err := models.NewMarkingDefinition(v) 130 | if err != nil { 131 | slogger.Error(fmt.Sprintf("Error creating marking definition: %s", err)) 132 | } 133 | e.Defintions = append(e.Defintions, markingDefinition) 134 | case "malware": 135 | malware, err := models.NewMalware(v) 136 | if err != nil { 137 | slogger.Error(fmt.Sprintf("Error creating malware: %s", err)) 138 | } 139 | e.Malwares = append(e.Malwares, malware) 140 | case "course-of-action": 141 | mitigation, err := models.NewMitigation(v) 142 | if err != nil { 143 | slogger.Error(fmt.Sprintf("Error creating mitigation: %s", err)) 144 | } 145 | e.Mitigations = append(e.Mitigations, mitigation) 146 | case "x-mitre-matrix": 147 | matrix, err := models.NewMatrix(v) 148 | if err != nil { 149 | slogger.Error(fmt.Sprintf("Error creating matrix: %s", err)) 150 | } 151 | e.Matrices = append(e.Matrices, matrix) 152 | case "relationship": 153 | relationship, err := models.NewRelationship(v) 154 | if err != nil { 155 | slogger.Error(fmt.Sprintf("Error creating relationship: %s", err)) 156 | } 157 | e.Relationships = append(e.Relationships, relationship) 158 | if relationship.RelationshipType != "revoked-by" { 159 | sourceID := relationship.SourceRef 160 | targetID := relationship.TargetRef 161 | if _, ok := e.attackRelationshipMap[sourceID]; !ok { 162 | e.attackRelationshipMap[sourceID] = []string{} 163 | } 164 | if _, ok := e.attackRelationshipMap[targetID]; !ok { 165 | e.attackRelationshipMap[targetID] = []string{} 166 | } 167 | found := false 168 | for _, val := range e.attackRelationshipMap[sourceID] { 169 | if val == targetID { 170 | found = true 171 | } 172 | } 173 | if !found { 174 | e.attackRelationshipMap[sourceID] = append(e.attackRelationshipMap[sourceID], targetID) 175 | } 176 | found = false 177 | for _, val := range e.attackRelationshipMap[targetID] { 178 | if val == sourceID { 179 | found = true 180 | } 181 | } 182 | if !found { 183 | e.attackRelationshipMap[targetID] = append(e.attackRelationshipMap[targetID], sourceID) 184 | } 185 | } 186 | case "x-mitre-tactic": 187 | tactic, err := models.NewTactic(v) 188 | if err != nil { 189 | slogger.Error(fmt.Sprintf("Error creating tactic: %s", err)) 190 | } 191 | e.Tactics = append(e.Tactics, tactic) 192 | case "attack-pattern": 193 | technique, err := models.NewTechnique(v) 194 | if err != nil { 195 | slogger.Error(fmt.Sprintf("Error creating technique: %s", err)) 196 | } 197 | e.Techniques = append(e.Techniques, technique) 198 | case "tool": 199 | tool, err := models.NewTool(v) 200 | if err != nil { 201 | slogger.Error(fmt.Sprintf("Error creating tool: %s", err)) 202 | } 203 | e.Tools = append(e.Tools, tool) 204 | } 205 | } 206 | e.setRelationships() 207 | return nil 208 | } 209 | 210 | // This method is used once we have collected our structs 211 | // to build relationships by collecting structs as properties 212 | // on the different entities 213 | func (e Enterprise) setRelationships() { 214 | e.setActorRelationships() 215 | e.setCampaignRelationships() 216 | e.setDataComponentRelationships() 217 | e.setDataSourceRelationships() 218 | e.setMalwareRelationships() 219 | e.setMitigationRelationships() 220 | e.setTacticRelationships() 221 | e.setTechniqueRelationships() 222 | e.setToolRelationships() 223 | } 224 | 225 | func (e Enterprise) setActorRelationships() { 226 | for _, actor := range e.Actors { 227 | if e.attackRelationshipMap[actor.Id] != nil { 228 | var malwares []models.Malware 229 | var techniques []models.Technique 230 | var tools []models.Tool 231 | for _, relationshipId := range e.attackRelationshipMap[actor.Id] { 232 | for _, malware := range e.Malwares { 233 | if malware.Id == relationshipId { 234 | malwares = append(malwares, malware) 235 | } 236 | } 237 | for _, tool := range e.Tools { 238 | if tool.Id == relationshipId { 239 | tools = append(tools, tool) 240 | } 241 | } 242 | for _, technique := range e.Techniques { 243 | if technique.Id == relationshipId { 244 | techniques = append(techniques, technique) 245 | } 246 | } 247 | } 248 | actor.Malwares = malwares 249 | actor.Tools = tools 250 | actor.Techniques = techniques 251 | } 252 | } 253 | } 254 | 255 | func (e Enterprise) setCampaignRelationships() { 256 | for _, campaign := range e.Campaigns { 257 | if e.attackRelationshipMap[campaign.Id] != nil { 258 | var malwares []models.Malware 259 | var tools []models.Tool 260 | var techniques []models.Technique 261 | for _, relationshipId := range e.attackRelationshipMap[campaign.Id] { 262 | for _, malware := range e.Malwares { 263 | if malware.Id == relationshipId { 264 | malwares = append(malwares, malware) 265 | } 266 | } 267 | for _, tool := range e.Tools { 268 | if tool.Id == relationshipId { 269 | tools = append(tools, tool) 270 | } 271 | } 272 | for _, technique := range e.Techniques { 273 | if technique.Id == relationshipId { 274 | techniques = append(techniques, technique) 275 | } 276 | } 277 | } 278 | campaign.Malwares = malwares 279 | campaign.Tools = tools 280 | campaign.Techniques = techniques 281 | } 282 | } 283 | } 284 | 285 | func (e Enterprise) setDataComponentRelationships() { 286 | for _, dataComponent := range e.DataComponents { 287 | if e.attackRelationshipMap[dataComponent.Id] != nil { 288 | var techniques []models.Technique 289 | for _, relationshipId := range e.attackRelationshipMap[dataComponent.Id] { 290 | for _, technique := range e.Techniques { 291 | if technique.Id == relationshipId { 292 | techniques = append(techniques, technique) 293 | } 294 | } 295 | } 296 | dataComponent.Techniques = techniques 297 | } 298 | } 299 | } 300 | 301 | func (e Enterprise) setDataSourceRelationships() { 302 | for _, dataSource := range e.DataSources { 303 | if e.attackRelationshipMap[dataSource.Id] != nil { 304 | var dataComponents []models.DataComponent 305 | var techniques []models.Technique 306 | for _, relationshipId := range e.attackRelationshipMap[dataSource.Id] { 307 | for _, dataComponent := range e.DataComponents { 308 | if dataComponent.Id == relationshipId { 309 | dataComponents = append(dataComponents, dataComponent) 310 | } 311 | } 312 | for _, technique := range e.Techniques { 313 | if technique.Id == relationshipId { 314 | techniques = append(techniques, technique) 315 | } 316 | } 317 | } 318 | dataSource.DataComponents = dataComponents 319 | dataSource.Techniques = techniques 320 | } 321 | } 322 | } 323 | 324 | func (e Enterprise) setMalwareRelationships() { 325 | for _, malware := range e.Malwares { 326 | if e.attackRelationshipMap[malware.Id] != nil { 327 | var actors []models.Actor 328 | var campaigns []models.Campaign 329 | var techniques []models.Technique 330 | for _, relationshipId := range e.attackRelationshipMap[malware.Id] { 331 | for _, actor := range e.Actors { 332 | if actor.Id == relationshipId { 333 | actors = append(actors, actor) 334 | } 335 | } 336 | for _, campaign := range e.Campaigns { 337 | if campaign.Id == relationshipId { 338 | campaigns = append(campaigns, campaign) 339 | } 340 | } 341 | for _, technique := range e.Techniques { 342 | if technique.Id == relationshipId { 343 | techniques = append(techniques, technique) 344 | } 345 | } 346 | } 347 | malware.Actors = actors 348 | malware.Campaigns = campaigns 349 | malware.Techniques = techniques 350 | } 351 | } 352 | } 353 | 354 | func (e Enterprise) setMitigationRelationships() { 355 | for _, mitigation := range e.Mitigations { 356 | var techniques []models.Technique 357 | if e.attackRelationshipMap[mitigation.Id] != nil { 358 | for _, relationshipId := range e.attackRelationshipMap[mitigation.Id] { 359 | for _, technique := range e.Techniques { 360 | if technique.Id == relationshipId { 361 | techniques = append(techniques, technique) 362 | } 363 | } 364 | } 365 | } 366 | if len(mitigation.ExternalReferences) > 0 { 367 | for _, rel := range mitigation.ExternalReferences { 368 | if rel.SourceName == "mitre-attack" { 369 | for _, technique := range e.Techniques { 370 | if technique.GetExternalID() == rel.ExternalId { 371 | techniques = append(techniques, technique) 372 | } 373 | } 374 | } 375 | } 376 | } 377 | mitigation.Techniques = techniques 378 | } 379 | } 380 | 381 | func (e Enterprise) setTacticRelationships() { 382 | for _, tactic := range e.Tactics { 383 | var techniques []models.Technique 384 | for _, technique := range e.Techniques { 385 | if technique.KillChainPhases != nil { 386 | for _, phase := range technique.KillChainPhases { 387 | if phase.PhaseName == tactic.XMitreShortname { 388 | techniques = append(techniques, technique) 389 | } 390 | } 391 | } 392 | } 393 | tactic.Techniques = techniques 394 | } 395 | } 396 | 397 | func (e Enterprise) setTechniqueRelationships() { 398 | for _, technique := range e.Techniques { 399 | if e.attackRelationshipMap[technique.Id] != nil { 400 | var actors []models.Actor 401 | var campaigns []models.Campaign 402 | var dataComponents []models.DataComponent 403 | var dataSources []models.DataSource 404 | var malwares []models.Malware 405 | var mitigations []models.Mitigation 406 | var tactics []models.Tactic 407 | var techniques []models.Technique 408 | var tools []models.Tool 409 | for _, relationshipId := range e.attackRelationshipMap[technique.Id] { 410 | for _, actor := range e.Actors { 411 | if actor.Id == relationshipId { 412 | actors = append(actors, actor) 413 | } 414 | } 415 | for _, campaign := range e.Campaigns { 416 | if campaign.Id == relationshipId { 417 | campaigns = append(campaigns, campaign) 418 | } 419 | } 420 | for _, dataComponent := range e.DataComponents { 421 | if dataComponent.Id == relationshipId { 422 | dataComponents = append(dataComponents, dataComponent) 423 | } 424 | } 425 | for _, dataSource := range e.DataSources { 426 | if dataSource.Id == relationshipId { 427 | dataSources = append(dataSources, dataSource) 428 | } 429 | } 430 | for _, malware := range e.Malwares { 431 | if malware.Id == relationshipId { 432 | malwares = append(malwares, malware) 433 | } 434 | } 435 | for _, mitigation := range e.Mitigations { 436 | if mitigation.Id == relationshipId { 437 | mitigations = append(mitigations, mitigation) 438 | } 439 | } 440 | for _, phase := range technique.KillChainPhases { 441 | for _, tactic := range e.Tactics { 442 | if tactic.XMitreShortname == phase.KillChainName { 443 | tactics = append(tactics, tactic) 444 | } 445 | } 446 | } 447 | for _, technique := range e.Techniques { 448 | if technique.Id == relationshipId { 449 | techniques = append(techniques, technique) 450 | } 451 | } 452 | for _, tool := range e.Tools { 453 | if tool.Id == relationshipId { 454 | tools = append(tools, tool) 455 | } 456 | } 457 | } 458 | technique.Actors = actors 459 | technique.Campaigns = campaigns 460 | technique.DataComponents = dataComponents 461 | technique.DataSources = dataSources 462 | technique.Malwares = malwares 463 | technique.Mitigations = mitigations 464 | technique.Tactics = tactics 465 | technique.Techniques = techniques 466 | technique.Tools = tools 467 | } 468 | } 469 | } 470 | 471 | func (e Enterprise) setToolRelationships() { 472 | for _, tool := range e.Tools { 473 | if e.attackRelationshipMap[tool.Id] != nil { 474 | var actors []models.Actor 475 | var campaigns []models.Campaign 476 | var techniques []models.Technique 477 | for _, relationshipId := range e.attackRelationshipMap[tool.Id] { 478 | for _, actor := range e.Actors { 479 | if actor.Id == relationshipId { 480 | actors = append(actors, actor) 481 | } 482 | } 483 | for _, campaign := range e.Campaigns { 484 | if campaign.Id == relationshipId { 485 | campaigns = append(campaigns, campaign) 486 | } 487 | } 488 | for _, technique := range e.Techniques { 489 | if technique.Id == relationshipId { 490 | techniques = append(techniques, technique) 491 | } 492 | } 493 | } 494 | tool.Actors = actors 495 | tool.Campaigns = campaigns 496 | tool.Techniques = techniques 497 | } 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /internal/menu/model.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | bubblelist "github.com/charmbracelet/bubbles/list" 9 | tea "github.com/charmbracelet/bubbletea" 10 | "github.com/charmbracelet/lipgloss" 11 | lipglosslist "github.com/charmbracelet/lipgloss/list" 12 | "golang.org/x/term" 13 | 14 | "github.com/msadministrator/goattck" 15 | ) 16 | 17 | type menuState int 18 | 19 | type item struct { 20 | title, desc string 21 | } 22 | 23 | func (i item) Title() string { return i.title } 24 | func (i item) Description() string { return i.desc } 25 | func (i item) FilterValue() string { return i.title } 26 | 27 | const ( 28 | stateSelectEntity menuState = iota 29 | stateSelectItem 30 | stateViewDetails 31 | ) 32 | 33 | type model struct { 34 | list bubblelist.Model 35 | tabs []mitreTab 36 | selectedTab int 37 | enterprise goattck.Enterprise 38 | entitySelected string 39 | selectedItem string 40 | state menuState 41 | } 42 | 43 | func (m model) Init() tea.Cmd { 44 | return nil 45 | } 46 | 47 | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 48 | switch msg := msg.(type) { 49 | case tea.KeyMsg: 50 | switch msg.String() { 51 | case "q", "ctrl+c": 52 | return m, tea.Quit 53 | case "esc": 54 | switch m.state { 55 | case stateSelectEntity: 56 | m.entitySelected = "" 57 | m.state = stateSelectEntity 58 | m.selectedItem = "" 59 | case stateSelectItem: 60 | m.state = stateSelectEntity 61 | m.selectedItem = "" 62 | case stateViewDetails: 63 | m.state = stateSelectItem 64 | m.selectedItem = "" 65 | } 66 | case "enter": 67 | switch m.state { 68 | case stateSelectEntity: 69 | if i, ok := m.list.SelectedItem().(item); ok { 70 | m.entitySelected = i.title 71 | m.state = stateSelectItem 72 | // Update list items based on selected entity 73 | m.updateEntityList() 74 | } 75 | case stateSelectItem: 76 | if i, ok := m.list.SelectedItem().(item); ok { 77 | m.selectedItem = i.title 78 | m.state = stateViewDetails 79 | } 80 | } 81 | case "right": 82 | if m.selectedTab < len(mitreTabs)-1 { 83 | m.selectedTab++ 84 | } 85 | case "left": 86 | if m.selectedTab > 0 { 87 | m.selectedTab-- 88 | } 89 | } 90 | case tea.WindowSizeMsg: 91 | h, v := docStyle.GetFrameSize() 92 | m.list.SetSize(msg.Width-h, msg.Height-v) 93 | } 94 | 95 | var cmd tea.Cmd 96 | m.list, cmd = m.list.Update(msg) 97 | return m, cmd 98 | } 99 | 100 | func (m *model) updateEntityList() { 101 | var items []bubblelist.Item 102 | switch m.entitySelected { 103 | case "actors": 104 | for _, actor := range m.enterprise.Actors { 105 | items = append(items, item{ 106 | title: actor.Name, 107 | desc: actor.Description, 108 | }) 109 | } 110 | case "campaigns": 111 | for _, campaign := range m.enterprise.Campaigns { 112 | items = append(items, item{ 113 | title: campaign.Name, 114 | desc: campaign.Description, 115 | }) 116 | } 117 | case "controls": 118 | for _, control := range m.enterprise.Controls { 119 | items = append(items, item{ 120 | title: control.Name, 121 | desc: "", 122 | }) 123 | } 124 | case "data sources": 125 | for _, source := range m.enterprise.DataSources { 126 | items = append(items, item{ 127 | title: source.Name, 128 | desc: source.Description, 129 | }) 130 | } 131 | case "malwares": 132 | for _, malware := range m.enterprise.Malwares { 133 | items = append(items, item{ 134 | title: malware.Name, 135 | desc: malware.Description, 136 | }) 137 | } 138 | case "mitigations": 139 | for _, mitigation := range m.enterprise.Mitigations { 140 | items = append(items, item{ 141 | title: mitigation.Name, 142 | desc: mitigation.Description, 143 | }) 144 | } 145 | case "tactics": 146 | for _, tactic := range m.enterprise.Tactics { 147 | items = append(items, item{ 148 | title: tactic.Name, 149 | desc: tactic.Description, 150 | }) 151 | } 152 | case "techniques": 153 | for _, technique := range m.enterprise.Techniques { 154 | items = append(items, item{ 155 | title: technique.Name, 156 | desc: technique.Description, 157 | }) 158 | } 159 | case "tools": 160 | for _, tool := range m.enterprise.Tools { 161 | items = append(items, item{ 162 | title: tool.Name, 163 | desc: tool.Description, 164 | }) 165 | } 166 | } 167 | m.list.SetItems(items) 168 | } 169 | 170 | func (m model) View() string { 171 | physicalWidth, _, _ := term.GetSize(int(os.Stdout.Fd())) 172 | doc := strings.Builder{} 173 | 174 | // Main content area 175 | switch m.state { 176 | case stateSelectEntity: 177 | // Selects the actors, techniques, tools, etc. entity 178 | doc.WriteString("Select an entity type:\n\n") 179 | doc.WriteString(m.list.View()) 180 | case stateSelectItem: 181 | // Selects the specific entity 182 | doc.WriteString(fmt.Sprintf("Select a %s:\n\n", m.entitySelected)) 183 | doc.WriteString(m.list.View()) 184 | case stateViewDetails: 185 | // View the details of the specific entity 186 | 187 | // We get our title on our detailed view 188 | doc.WriteString(m.getEntityTitle() + "\n\n") 189 | 190 | // Add the selected entity string 191 | doc.WriteString(fmt.Sprintf("Details for %s: %s\n\n", m.entitySelected, m.selectedItem)) 192 | 193 | // Build our tabs 194 | doc.WriteString(m.getEntityTabs() + "\n\n") 195 | 196 | // Display entity details based on type 197 | doc.WriteString(m.getEntityDetails()) 198 | } 199 | 200 | // At the end of each detailed entity view, we display a status bar 201 | doc.WriteString(m.getStaturBar()) 202 | 203 | // Now we set the width and write our doc string to the console 204 | if physicalWidth > 0 { 205 | docStyle = docStyle.MaxWidth(physicalWidth) 206 | } 207 | 208 | return docStyle.Render(doc.String()) 209 | } 210 | 211 | // Builds the bottom status bar 212 | func (m model) getStaturBar() string { 213 | w := lipgloss.Width 214 | 215 | statusKey := statusStyle.Render("STATUS") 216 | encoding := encodingStyle.Render("UTF-8") 217 | state := statusText.Render(m.getStateString()) 218 | statusVal := statusText. 219 | Width(width - w(statusKey) - w(encoding)). 220 | Render(state) 221 | 222 | bar := lipgloss.JoinHorizontal(lipgloss.Top, 223 | statusKey, 224 | statusVal, 225 | encoding, 226 | ) 227 | 228 | return statusBarStyle.Width(width).Render(bar) 229 | } 230 | 231 | // Builds the different tabs on the detail entity view 232 | func (m model) getEntityTabs() string { 233 | var row string 234 | switch m.selectedTab { 235 | case 0: 236 | row = overviewTab.GetRow() 237 | case 1: 238 | row = relationshipsTab.GetRow() 239 | case 2: 240 | row = referencesTab.GetRow() 241 | case 3: 242 | row = extendedTab.GetRow() 243 | case 4: 244 | row = externalTab.GetRow() 245 | } 246 | 247 | gap := tabGap.Render(strings.Repeat(" ", max(0, width-lipgloss.Width(row)-2))) 248 | return lipgloss.JoinHorizontal(lipgloss.Bottom, row, gap) 249 | } 250 | 251 | // Builds the entity title section of our details display 252 | func (m model) getEntityTitle() string { 253 | var ( 254 | colors = colorGrid(1, 5) 255 | title strings.Builder 256 | ) 257 | 258 | // We get the string value berfore loading our menu 259 | titleStyle = titleStyle.SetString(m.getStateStringForTitle()) 260 | 261 | for i, v := range colors { 262 | const offset = 2 263 | c := lipgloss.Color(v[0]) 264 | fmt.Fprint(&title, titleStyle.MarginLeft(i*offset).Background(c)) 265 | if i < len(colors)-1 { 266 | title.WriteRune('\n') 267 | } 268 | } 269 | 270 | desc := lipgloss.JoinVertical(lipgloss.Left, 271 | descStyle.Render("MITRE ATT&CK Enterprise Framework"), 272 | infoStyle.Render("Interactive Menu System"), 273 | ) 274 | 275 | return lipgloss.JoinHorizontal(lipgloss.Top, title.String(), desc) 276 | } 277 | 278 | // Returns a string for use when building the entity title 279 | func (m model) getStateStringForTitle() string { 280 | switch m.entitySelected { 281 | case "actors": 282 | return "Actor" 283 | case "campaigns": 284 | return "Campaign" 285 | case "data sources": 286 | return "Data Source" 287 | case "malwares": 288 | return "Malware" 289 | case "mitigations": 290 | return "Mitigation" 291 | case "tactics": 292 | return "Tactic" 293 | case "techniques": 294 | return "Technique" 295 | case "tools": 296 | return "Tool" 297 | default: 298 | return "Unknown" 299 | } 300 | } 301 | 302 | // Displays the bottom status bar representing the current state 303 | func (m model) getStateString() string { 304 | switch m.state { 305 | case stateSelectEntity: 306 | return "Select Entity Type" 307 | case stateSelectItem: 308 | return fmt.Sprintf("Select %s", m.entitySelected) 309 | case stateViewDetails: 310 | return fmt.Sprintf("Viewing %s: %s", m.entitySelected, m.selectedItem) 311 | default: 312 | return "Unknown State" 313 | } 314 | } 315 | 316 | func (m model) getEntityDetails() string { 317 | var details strings.Builder 318 | 319 | switch m.entitySelected { 320 | case "actors": 321 | for _, actor := range m.enterprise.Actors { 322 | if actor.Name == m.selectedItem { 323 | switch m.selectedTab { 324 | case 0: // Overview tab 325 | 326 | // Set the description first 327 | details.WriteString(descriptionStyle.Width(width).Render(fmt.Sprintf("%s", actor.Description))) 328 | details.WriteString("\n") 329 | 330 | // We now have a list of different attributers we want to display 331 | // in our table here 332 | attributes := map[string]string{ 333 | "Id": actor.Id, 334 | "Type": actor.Type, 335 | "Created": actor.Created, 336 | "Modified": actor.Modified, 337 | "Version": actor.XMitreVersion, 338 | "Domains": strings.Join(actor.XMitreDomains, ","), 339 | "Revoked": fmt.Sprintf("%v", actor.Revoked), 340 | "Deprecated": fmt.Sprintf("%v", actor.XMitreDeprecated), 341 | "CreatedByRef": actor.CreatedByRef, 342 | "ObjectMarkingrefs": strings.Join(actor.ObjectMarkingRefs, ""), 343 | } 344 | 345 | rows := [][]string{} 346 | for key, val := range attributes { 347 | rows = append(rows, []string{ 348 | key, 349 | val, 350 | }) 351 | } 352 | 353 | // we display the table 354 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 355 | 356 | details.WriteString("\n") 357 | 358 | // If we have aliases, we display them 359 | if len(actor.Aliases) > 0 { 360 | aList := []string{} 361 | aList = append(aList, actor.Aliases...) 362 | details.WriteString(lipglosslist.New("Aliases", lipglosslist.New(aList)).String()) 363 | } 364 | details.WriteString("\n\n") 365 | case 1: // Relationships tab 366 | var malwareList, toolList, techniqueList []string 367 | 368 | if len(actor.Malwares) > 0 { 369 | for _, malware := range actor.Malwares { 370 | malwareList = append(malwareList, fmt.Sprintf("%s (%s)", malware.Name, malware.GetExternalID())) 371 | } 372 | } 373 | 374 | if len(actor.Tools) > 0 { 375 | for _, tool := range actor.Tools { 376 | toolList = append(toolList, fmt.Sprintf("%s (%s)", tool.Name, tool.GetExternalID())) 377 | } 378 | } 379 | 380 | if len(actor.Techniques) > 0 { 381 | for _, technique := range actor.Techniques { 382 | techniqueList = append(techniqueList, fmt.Sprintf("%s (%s)", technique.Name, technique.GetExternalID())) 383 | } 384 | } 385 | 386 | l := lipglosslist.New( 387 | "Malwares", lipglosslist.New(malwareList), 388 | "Tools", lipglosslist.New(toolList), 389 | "Techniques", lipglosslist.New(techniqueList), 390 | ) 391 | 392 | details.WriteString(l.String()) 393 | details.WriteString("\n\n") 394 | case 2: // References tab 395 | if len(actor.ExternalReferences) > 0 { 396 | l := lipglosslist.New() 397 | for _, ref := range actor.ExternalReferences { 398 | if ref.Url != "" { 399 | l.Item(fmt.Sprintf("[%s](%s)", ref.SourceName, ref.Url)) 400 | } 401 | } 402 | details.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, l.String())) 403 | } 404 | details.WriteString("\n\n") 405 | case 3: // Extended tab 406 | // This section displays extra attributes 407 | rows := [][]string{} 408 | 409 | attributes := map[string]string{ 410 | "Contributors": strings.Join(actor.XMitreContributors, ","), 411 | "XMitreModifiedByRef": actor.XMitreModifiedByRef, 412 | "XMitreAttackSpecVersion": actor.XMitreAttackSpecVersion, 413 | } 414 | 415 | for key, val := range attributes { 416 | rows = append(rows, []string{ 417 | key, 418 | val, 419 | }) 420 | } 421 | 422 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 423 | details.WriteString("\n\n") 424 | case 4: // External 425 | // TODO 426 | } 427 | } 428 | } 429 | case "campaigns": 430 | for _, campaign := range m.enterprise.Campaigns { 431 | if campaign.Name == m.selectedItem { 432 | switch m.selectedTab { 433 | case 0: // Overview 434 | // Set the description first 435 | details.WriteString(descriptionStyle.Width(width).Render(fmt.Sprintf("%s", campaign.Description))) 436 | details.WriteString("\n") 437 | 438 | // We now have a list of different attributers we want to display 439 | // in our table here 440 | attributes := map[string]string{ 441 | "Id": campaign.Id, 442 | "Type": campaign.Type, 443 | "Created": campaign.Created, 444 | "Modified": campaign.Modified, 445 | "Version": campaign.XMitreVersion, 446 | "Domains": strings.Join(campaign.XMitreDomains, ","), 447 | "Revoked": fmt.Sprintf("%v", campaign.Revoked), 448 | "Deprecated": fmt.Sprintf("%v", campaign.XMitreDeprecated), 449 | "CreatedByRef": campaign.CreatedByRef, 450 | "ObjectMarkingrefs": strings.Join(campaign.ObjectMarkingRefs, ""), 451 | } 452 | 453 | rows := [][]string{} 454 | for key, val := range attributes { 455 | rows = append(rows, []string{ 456 | key, 457 | val, 458 | }) 459 | } 460 | 461 | // we display the table 462 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 463 | 464 | details.WriteString("\n\n") 465 | 466 | case 1: // Relationships 467 | var malwareList, toolList, techniqueList []string 468 | 469 | if len(campaign.Malwares) > 0 { 470 | for _, malware := range campaign.Malwares { 471 | malwareList = append(malwareList, fmt.Sprintf("%s (%s)", malware.Name, malware.GetExternalID())) 472 | } 473 | } 474 | 475 | if len(campaign.Tools) > 0 { 476 | for _, tool := range campaign.Tools { 477 | toolList = append(toolList, fmt.Sprintf("%s (%s)", tool.Name, tool.GetExternalID())) 478 | } 479 | } 480 | 481 | if len(campaign.Techniques) > 0 { 482 | for _, technique := range campaign.Techniques { 483 | techniqueList = append(techniqueList, fmt.Sprintf("%s (%s)", technique.Name, technique.GetExternalID())) 484 | } 485 | } 486 | 487 | l := lipglosslist.New( 488 | "Malwares", lipglosslist.New(malwareList), 489 | "Tools", lipglosslist.New(toolList), 490 | "Techniques", lipglosslist.New(techniqueList), 491 | ) 492 | 493 | details.WriteString(l.String()) 494 | details.WriteString("\n\n") 495 | 496 | case 2: // References 497 | if len(campaign.ExternalReferences) > 0 { 498 | l := lipglosslist.New() 499 | for _, ref := range campaign.ExternalReferences { 500 | if ref.Url != "" { 501 | l.Item(fmt.Sprintf("[%s](%s)", ref.SourceName, ref.Url)) 502 | } 503 | } 504 | details.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, l.String())) 505 | } 506 | details.WriteString("\n\n") 507 | 508 | case 3: // Extended 509 | // This section displays extra attributes 510 | rows := [][]string{} 511 | 512 | attributes := map[string]string{ 513 | "First Seen": campaign.FirstSeen.String(), 514 | "First Seen Citation": campaign.XMitreFirstSeenCitation, 515 | "Last Seen": campaign.LastSeen.String(), 516 | "Last Seen Citation": campaign.XMitreLastSeenCitation, 517 | "Modified by Reference": campaign.XMitreModifiedByRef, 518 | "Attack Spec Version": campaign.XMitreAttackSpecVersion, 519 | } 520 | 521 | for key, val := range attributes { 522 | rows = append(rows, []string{ 523 | key, 524 | val, 525 | }) 526 | } 527 | 528 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 529 | details.WriteString("\n\n") 530 | case 4: // External 531 | //TODO 532 | } 533 | } 534 | } 535 | case "data sources": 536 | for _, source := range m.enterprise.DataSources { 537 | if source.Name == m.selectedItem { 538 | switch m.selectedTab { 539 | case 0: // Overview 540 | // Set the description first 541 | details.WriteString(descriptionStyle.Width(width).Render(fmt.Sprintf("%s", source.Description))) 542 | details.WriteString("\n") 543 | 544 | // We now have a list of different attributers we want to display 545 | // in our table here 546 | attributes := map[string]string{ 547 | "Id": source.Id, 548 | "Type": source.Type, 549 | "Created": source.Created, 550 | "Modified": source.Modified, 551 | "Version": source.XMitreVersion, 552 | "Domains": strings.Join(source.XMitreDomains, ","), 553 | "Revoked": fmt.Sprintf("%v", source.Revoked), 554 | "Deprecated": fmt.Sprintf("%v", source.XMitreDeprecated), 555 | "CreatedByRef": source.CreatedByRef, 556 | "ObjectMarkingrefs": strings.Join(source.ObjectMarkingRefs, ""), 557 | } 558 | 559 | rows := [][]string{} 560 | for key, val := range attributes { 561 | rows = append(rows, []string{ 562 | key, 563 | val, 564 | }) 565 | } 566 | 567 | // we display the table 568 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 569 | 570 | details.WriteString("\n\n") 571 | 572 | case 1: // Relationships 573 | var componentList, techniqueList []string 574 | 575 | if len(source.DataComponents) > 0 { 576 | for _, malware := range source.DataComponents { 577 | componentList = append(componentList, fmt.Sprintf("%s (%s)", malware.Name, malware.GetExternalID())) 578 | } 579 | } 580 | 581 | if len(source.Techniques) > 0 { 582 | for _, source := range source.Techniques { 583 | techniqueList = append(techniqueList, fmt.Sprintf("%s", source.Name)) 584 | } 585 | } 586 | 587 | l := lipglosslist.New( 588 | "Data Components", lipglosslist.New(componentList), 589 | "Techniques", lipglosslist.New(techniqueList), 590 | ) 591 | 592 | details.WriteString(l.String()) 593 | details.WriteString("\n\n") 594 | 595 | case 2: // References 596 | if len(source.ExternalReferences) > 0 { 597 | l := lipglosslist.New() 598 | for _, ref := range source.ExternalReferences { 599 | if ref.Url != "" { 600 | l.Item(fmt.Sprintf("[%s](%s)", ref.SourceName, ref.Url)) 601 | } 602 | } 603 | details.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, l.String())) 604 | } 605 | details.WriteString("\n\n") 606 | 607 | case 3: // Extended 608 | // This section displays extra attributes 609 | rows := [][]string{} 610 | 611 | attributes := map[string]string{ 612 | "Contributors": strings.Join(source.XMitreContributors, ","), 613 | "Platforms": strings.Join(source.XMitrePlatforms, ","), 614 | "Modified by Reference": source.XMitreModifiedByRef, 615 | "Attack Spec Version": source.XMitreAttackSpecVersion, 616 | } 617 | 618 | for key, val := range attributes { 619 | rows = append(rows, []string{ 620 | key, 621 | val, 622 | }) 623 | } 624 | 625 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 626 | details.WriteString("\n\n") 627 | case 4: // External 628 | //TODO 629 | } 630 | } 631 | } 632 | case "techniques": 633 | for _, technique := range m.enterprise.Techniques { 634 | if technique.Name == m.selectedItem { 635 | switch m.selectedTab { 636 | case 0: // Overview 637 | // Set the description first 638 | details.WriteString(descriptionStyle.Width(width).Render(fmt.Sprintf("%s", technique.Description))) 639 | details.WriteString("\n") 640 | 641 | // We now have a list of different attributers we want to display 642 | // in our table here 643 | attributes := map[string]string{ 644 | "Id": technique.Id, 645 | "Type": technique.Type, 646 | "Created": technique.Created, 647 | "Modified": technique.Modified, 648 | "Version": technique.XMitreVersion, 649 | "Domains": strings.Join(technique.XMitreDomains, ","), 650 | "Revoked": fmt.Sprintf("%v", technique.Revoked), 651 | "Deprecated": fmt.Sprintf("%v", technique.XMitreDeprecated), 652 | "CreatedByRef": technique.CreatedByRef, 653 | "ObjectMarkingrefs": strings.Join(technique.ObjectMarkingRefs, ""), 654 | "IsSubtechnique": fmt.Sprintf("%v", technique.XMitreIsSubtechnique), 655 | } 656 | 657 | rows := [][]string{} 658 | for key, val := range attributes { 659 | rows = append(rows, []string{ 660 | key, 661 | val, 662 | }) 663 | } 664 | 665 | // we display the table 666 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 667 | 668 | details.WriteString("\n\n") 669 | 670 | case 1: // Relationships 671 | var actorList, malwareList, toolList, datasourceList []string 672 | 673 | if len(technique.Malwares) > 0 { 674 | for _, malware := range technique.Malwares { 675 | malwareList = append(malwareList, fmt.Sprintf("%s (%s)", malware.Name, malware.GetExternalID())) 676 | } 677 | } 678 | 679 | if len(technique.Tools) > 0 { 680 | for _, tool := range technique.Tools { 681 | toolList = append(toolList, fmt.Sprintf("%s (%s)", tool.Name, tool.GetExternalID())) 682 | } 683 | } 684 | 685 | if len(technique.Actors) > 0 { 686 | for _, actor := range technique.Actors { 687 | actorList = append(actorList, fmt.Sprintf("%s (%s)", actor.Name, actor.GetExternalID())) 688 | } 689 | } 690 | 691 | if len(technique.DataSources) > 0 { 692 | for _, source := range technique.DataSources { 693 | datasourceList = append(datasourceList, fmt.Sprintf("%s", source.Name)) 694 | } 695 | } 696 | 697 | l := lipglosslist.New( 698 | "Malwares", lipglosslist.New(malwareList), 699 | "Tools", lipglosslist.New(toolList), 700 | "Actors", lipglosslist.New(actorList), 701 | "Data Sources", lipglosslist.New(datasourceList), 702 | ) 703 | 704 | details.WriteString(l.String()) 705 | details.WriteString("\n\n") 706 | 707 | case 2: // References 708 | if len(technique.ExternalReferences) > 0 { 709 | l := lipglosslist.New() 710 | for _, ref := range technique.ExternalReferences { 711 | if ref.Url != "" { 712 | l.Item(fmt.Sprintf("[%s](%s)", ref.SourceName, ref.Url)) 713 | } 714 | } 715 | details.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, l.String())) 716 | } 717 | details.WriteString("\n\n") 718 | 719 | case 3: // Extended 720 | // This section displays extra attributes 721 | rows := [][]string{} 722 | 723 | attributes := map[string]string{ 724 | "Contributors": strings.Join(technique.XMitreContributors, ","), 725 | "Modified by Reference": technique.XMitreModifiedByRef, 726 | "Attack Spec Version": technique.XMitreAttackSpecVersion, 727 | "Data Sources": strings.Join(technique.XMitreDataSources, ","), 728 | "Effective Permissions": strings.Join(technique.XMitreEffectivePermissions, ","), 729 | "Remote Support": fmt.Sprintf("%v", technique.XMitreRemoteSupport), 730 | "Permissions Required": strings.Join(technique.XMitrePermissionsRequired, ","), 731 | "Detection": technique.XMitreDetection, 732 | "Defense Bypassed": strings.Join(technique.XMitreDefenseBypassed, ","), 733 | "System Requirements": strings.Join(technique.XMitreSystemRequirements, ","), 734 | "Platforms": strings.Join(technique.XMitrePlatforms, ","), 735 | "Network Requirements": fmt.Sprintf("%v", technique.XMitreNetworkRequirements), 736 | } 737 | 738 | for key, val := range attributes { 739 | rows = append(rows, []string{ 740 | key, 741 | val, 742 | }) 743 | } 744 | 745 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 746 | details.WriteString("\n\n") 747 | case 4: // External 748 | //TODO 749 | } 750 | } 751 | } 752 | case "malwares": 753 | for _, malware := range m.enterprise.Malwares { 754 | if malware.Name == m.selectedItem { 755 | switch m.selectedTab { 756 | case 0: // Overview 757 | // Set the description first 758 | details.WriteString(descriptionStyle.Width(width).Render(fmt.Sprintf("%s", malware.Description))) 759 | details.WriteString("\n") 760 | 761 | // We now have a list of different attributers we want to display 762 | // in our table here 763 | attributes := map[string]string{ 764 | "Id": malware.Id, 765 | "Type": malware.Type, 766 | "Created": malware.Created, 767 | "Modified": malware.Modified, 768 | "Version": malware.XMitreVersion, 769 | "Domains": strings.Join(malware.XMitreDomains, ","), 770 | "Revoked": fmt.Sprintf("%v", malware.Revoked), 771 | "Deprecated": fmt.Sprintf("%v", malware.XMitreDeprecated), 772 | "CreatedByRef": malware.CreatedByRef, 773 | "ObjectMarkingrefs": strings.Join(malware.ObjectMarkingRefs, ""), 774 | } 775 | 776 | rows := [][]string{} 777 | for key, val := range attributes { 778 | rows = append(rows, []string{ 779 | key, 780 | val, 781 | }) 782 | } 783 | 784 | // we display the table 785 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 786 | 787 | details.WriteString("\n\n") 788 | case 1: // Relationships 789 | var actorList, campaignList, techniqueList []string 790 | 791 | if len(malware.Actors) > 0 { 792 | for _, malware := range malware.Actors { 793 | actorList = append(actorList, fmt.Sprintf("%s (%s)", malware.Name, malware.GetExternalID())) 794 | } 795 | } 796 | 797 | if len(malware.Campaigns) > 0 { 798 | for _, tool := range malware.Campaigns { 799 | campaignList = append(campaignList, fmt.Sprintf("%s (%s)", tool.Name, tool.GetExternalID())) 800 | } 801 | } 802 | 803 | if len(malware.Techniques) > 0 { 804 | for _, technique := range malware.Techniques { 805 | techniqueList = append(techniqueList, fmt.Sprintf("%s (%s)", technique.Name, technique.GetExternalID())) 806 | } 807 | } 808 | 809 | l := lipglosslist.New( 810 | "Actors", lipglosslist.New(actorList), 811 | "Campaigns", lipglosslist.New(campaignList), 812 | "Techniques", lipglosslist.New(techniqueList), 813 | ) 814 | 815 | details.WriteString(l.String()) 816 | details.WriteString("\n\n") 817 | case 2: // References 818 | if len(malware.ExternalReferences) > 0 { 819 | l := lipglosslist.New() 820 | for _, ref := range malware.ExternalReferences { 821 | if ref.Url != "" { 822 | l.Item(fmt.Sprintf("[%s](%s)", ref.SourceName, ref.Url)) 823 | } 824 | } 825 | details.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, l.String())) 826 | } 827 | details.WriteString("\n\n") 828 | case 3: // Extended 829 | // This section displays extra attributes 830 | rows := [][]string{} 831 | 832 | attributes := map[string]string{ 833 | "Contributors": strings.Join(malware.XMitreContributors, ","), 834 | "Modified by Reference": malware.XMitreModifiedByRef, 835 | "Attack Spec Version": malware.XMitreAttackSpecVersion, 836 | } 837 | 838 | for key, val := range attributes { 839 | rows = append(rows, []string{ 840 | key, 841 | val, 842 | }) 843 | } 844 | 845 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 846 | details.WriteString("\n\n") 847 | case 4: // External 848 | // TODO 849 | } 850 | } 851 | } 852 | // Add more cases for other entity types as needed 853 | case "mitigations": 854 | for _, mitigation := range m.enterprise.Mitigations { 855 | if mitigation.Name == m.selectedItem { 856 | switch m.selectedTab { 857 | case 0: // Overview 858 | // Set the description first 859 | details.WriteString(descriptionStyle.Width(width).Render(fmt.Sprintf("%s", mitigation.Description))) 860 | details.WriteString("\n") 861 | 862 | // We now have a list of different attributers we want to display 863 | // in our table here 864 | attributes := map[string]string{ 865 | "Id": mitigation.Id, 866 | "Type": mitigation.Type, 867 | "Created": mitigation.Created, 868 | "Modified": mitigation.Modified, 869 | "Version": mitigation.XMitreVersion, 870 | "Domains": strings.Join(mitigation.XMitreDomains, ","), 871 | "Revoked": fmt.Sprintf("%v", mitigation.Revoked), 872 | "Deprecated": fmt.Sprintf("%v", mitigation.XMitreDeprecated), 873 | "CreatedByRef": mitigation.CreatedByRef, 874 | "ObjectMarkingrefs": strings.Join(mitigation.ObjectMarkingRefs, ""), 875 | } 876 | 877 | rows := [][]string{} 878 | for key, val := range attributes { 879 | rows = append(rows, []string{ 880 | key, 881 | val, 882 | }) 883 | } 884 | 885 | // we display the table 886 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 887 | 888 | details.WriteString("\n\n") 889 | case 1: // Relationships 890 | var techniqueList []string 891 | 892 | if len(mitigation.Techniques) > 0 { 893 | for _, technique := range mitigation.Techniques { 894 | techniqueList = append(techniqueList, fmt.Sprintf("%s (%s)", technique.Name, technique.GetExternalID())) 895 | } 896 | } 897 | 898 | l := lipglosslist.New( 899 | "Techniques", lipglosslist.New(techniqueList), 900 | ) 901 | 902 | details.WriteString(l.String()) 903 | details.WriteString("\n\n") 904 | case 2: // References 905 | if len(mitigation.ExternalReferences) > 0 { 906 | l := lipglosslist.New() 907 | for _, ref := range mitigation.ExternalReferences { 908 | if ref.Url != "" { 909 | l.Item(fmt.Sprintf("[%s](%s)", ref.SourceName, ref.Url)) 910 | } 911 | } 912 | details.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, l.String())) 913 | } 914 | details.WriteString("\n\n") 915 | case 3: // Extended 916 | // This section displays extra attributes 917 | rows := [][]string{} 918 | 919 | attributes := map[string]string{ 920 | "Modified by Reference": mitigation.XMitreModifiedByRef, 921 | "Attack Spec Version": mitigation.XMitreAttackSpecVersion, 922 | } 923 | 924 | for key, val := range attributes { 925 | rows = append(rows, []string{ 926 | key, 927 | val, 928 | }) 929 | } 930 | 931 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 932 | details.WriteString("\n\n") 933 | case 4: // External 934 | // TODO 935 | } 936 | } 937 | } 938 | case "tactics": 939 | for _, tactic := range m.enterprise.Tactics { 940 | if tactic.Name == m.selectedItem { 941 | switch m.selectedTab { 942 | case 0: // Overview 943 | // Set the description first 944 | details.WriteString(descriptionStyle.Width(width).Render(fmt.Sprintf("%s", tactic.Description))) 945 | details.WriteString("\n") 946 | 947 | // We now have a list of different attributers we want to display 948 | // in our table here 949 | attributes := map[string]string{ 950 | "Id": tactic.Id, 951 | "Type": tactic.Type, 952 | "Created": tactic.Created, 953 | "Modified": tactic.Modified, 954 | "Version": tactic.XMitreVersion, 955 | "Domains": strings.Join(tactic.XMitreDomains, ","), 956 | "CreatedByRef": tactic.CreatedByRef, 957 | "ObjectMarkingrefs": strings.Join(tactic.ObjectMarkingRefs, ""), 958 | } 959 | 960 | rows := [][]string{} 961 | for key, val := range attributes { 962 | rows = append(rows, []string{ 963 | key, 964 | val, 965 | }) 966 | } 967 | 968 | // we display the table 969 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 970 | 971 | details.WriteString("\n\n") 972 | case 1: // Relationships 973 | var techniqueList []string 974 | 975 | if len(tactic.Techniques) > 0 { 976 | for _, technique := range tactic.Techniques { 977 | techniqueList = append(techniqueList, fmt.Sprintf("%s (%s)", technique.Name, technique.GetExternalID())) 978 | } 979 | } 980 | 981 | l := lipglosslist.New( 982 | "Techniques", lipglosslist.New(techniqueList), 983 | ) 984 | 985 | details.WriteString(l.String()) 986 | details.WriteString("\n\n") 987 | case 2: // References 988 | if len(tactic.ExternalReferences) > 0 { 989 | l := lipglosslist.New() 990 | for _, ref := range tactic.ExternalReferences { 991 | if ref.Url != "" { 992 | l.Item(fmt.Sprintf("[%s](%s)", ref.SourceName, ref.Url)) 993 | } 994 | } 995 | details.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, l.String())) 996 | } 997 | details.WriteString("\n\n") 998 | case 3: // Extended 999 | // This section displays extra attributes 1000 | rows := [][]string{} 1001 | 1002 | attributes := map[string]string{ 1003 | "Modified by Reference": tactic.XMitreModifiedByRef, 1004 | "Attack Spec Version": tactic.XMitreAttackSpecVersion, 1005 | } 1006 | 1007 | for key, val := range attributes { 1008 | rows = append(rows, []string{ 1009 | key, 1010 | val, 1011 | }) 1012 | } 1013 | 1014 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 1015 | details.WriteString("\n\n") 1016 | case 4: // External 1017 | // TODO 1018 | } 1019 | } 1020 | } 1021 | case "tools": 1022 | for _, tool := range m.enterprise.Tools { 1023 | if tool.Name == m.selectedItem { 1024 | switch m.selectedTab { 1025 | case 0: // Overview 1026 | // Set the description first 1027 | details.WriteString(descriptionStyle.Width(width).Render(fmt.Sprintf("%s", tool.Description))) 1028 | details.WriteString("\n") 1029 | 1030 | // We now have a list of different attributers we want to display 1031 | // in our table here 1032 | attributes := map[string]string{ 1033 | "Id": tool.Id, 1034 | "Type": tool.Type, 1035 | "Created": tool.Created, 1036 | "Modified": tool.Modified, 1037 | "Version": tool.XMitreVersion, 1038 | "Domains": strings.Join(tool.XMitreDomains, ","), 1039 | "Revoked": fmt.Sprintf("%v", tool.Revoked), 1040 | "Deprecated": fmt.Sprintf("%v", tool.XMitreDeprecated), 1041 | "CreatedByRef": tool.CreatedByRef, 1042 | "ObjectMarkingrefs": strings.Join(tool.ObjectMarkingRefs, ""), 1043 | } 1044 | 1045 | rows := [][]string{} 1046 | for key, val := range attributes { 1047 | rows = append(rows, []string{ 1048 | key, 1049 | val, 1050 | }) 1051 | } 1052 | 1053 | // we display the table 1054 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 1055 | 1056 | details.WriteString("\n\n") 1057 | case 1: // Relationships 1058 | var actorList, campaignList, techniqueList []string 1059 | 1060 | if len(tool.Actors) > 0 { 1061 | for _, technique := range tool.Actors { 1062 | actorList = append(actorList, fmt.Sprintf("%s (%s)", technique.Name, technique.GetExternalID())) 1063 | } 1064 | } 1065 | 1066 | if len(tool.Campaigns) > 0 { 1067 | for _, technique := range tool.Campaigns { 1068 | campaignList = append(campaignList, fmt.Sprintf("%s (%s)", technique.Name, technique.GetExternalID())) 1069 | } 1070 | } 1071 | 1072 | if len(tool.Techniques) > 0 { 1073 | for _, technique := range tool.Techniques { 1074 | techniqueList = append(techniqueList, fmt.Sprintf("%s (%s)", technique.Name, technique.GetExternalID())) 1075 | } 1076 | } 1077 | 1078 | l := lipglosslist.New( 1079 | "Actors", lipglosslist.New(actorList), 1080 | "Campaigns", lipglosslist.New(campaignList), 1081 | "Techniques", lipglosslist.New(techniqueList), 1082 | ) 1083 | 1084 | details.WriteString(l.String()) 1085 | details.WriteString("\n\n") 1086 | case 2: // References 1087 | if len(tool.ExternalReferences) > 0 { 1088 | l := lipglosslist.New() 1089 | for _, ref := range tool.ExternalReferences { 1090 | if ref.Url != "" { 1091 | l.Item(fmt.Sprintf("[%s](%s)", ref.SourceName, ref.Url)) 1092 | } 1093 | } 1094 | details.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, l.String())) 1095 | } 1096 | details.WriteString("\n\n") 1097 | case 3: // Extended 1098 | // This section displays extra attributes 1099 | rows := [][]string{} 1100 | 1101 | attributes := map[string]string{ 1102 | "Modified by Reference": tool.XMitreModifiedByRef, 1103 | "Attack Spec Version": tool.XMitreAttackSpecVersion, 1104 | } 1105 | 1106 | for key, val := range attributes { 1107 | rows = append(rows, []string{ 1108 | key, 1109 | val, 1110 | }) 1111 | } 1112 | 1113 | details.WriteString(getFormattedTable([]string{"Attribute", "Value"}, rows)) 1114 | details.WriteString("\n\n") 1115 | case 4: // External 1116 | // TODO 1117 | } 1118 | } 1119 | } 1120 | } 1121 | 1122 | return details.String() 1123 | } 1124 | --------------------------------------------------------------------------------