package rules import ( "bytes" "crypto/tls" "encoding/json" "errors" "fmt" "io/ioutil" "iwarma.ru/console/correlator/events" "net/http" "strconv" "text/template" "iwarma.ru/console/correlator/config" log "github.com/sirupsen/logrus" "github.com/spf13/viper" ) const ( IncidentActionType = "incident" ) type IncidentAction struct { Title string `json:"title"` // template Comment string `json:"comment"` // template Effects []string `json:"effects"` Category string `json:"category"` Importance string `json:"importance"` Assigned string `json:"assigned_to"` Description string `json:"description"` // template CloseRecommendations []string `json:"close_recommendations"` // Private token string client *http.Client template struct { title *template.Template comment *template.Template description *template.Template } } func (action IncidentAction) GetType() string { return IncidentActionType } func (action IncidentAction) ToInterface() (map[string]interface{}, error) { result := make(map[string]interface{}) result["title"] = action.Title if action.Comment != "" { result["comment"] = action.Comment } // Effects if len(action.Effects) > 0 { effects := make([]interface{}, 0) for _, cur := range action.Effects { tmp, err := strconv.Atoi(cur) if err != nil { return nil, err } effects = append(effects, tmp) } result["effects"] = effects } // Category if action.Category != "" { catPk, err := strconv.Atoi(action.Category) result["category"] = catPk if err != nil { return nil, err } } // Importance imp, err := strconv.Atoi(action.Importance) if err != nil { return nil, err } result["importance"] = imp // Assigned if action.Assigned != "" { ass, err := strconv.Atoi(action.Assigned) if err != nil { return nil, err } result["assigned_to"] = ass } // Description if action.Description != "" { result["description"] = action.Description } // Close recommendations if len(action.CloseRecommendations) > 0 { closeRec := make([]interface{}, 0) for _, cur := range action.CloseRecommendations { tmp, err := strconv.Atoi(cur) if err != nil { return nil, err } closeRec = append(closeRec, tmp) } result["close_recommendations"] = closeRec } return result, nil } func (action IncidentAction) MarshalJSON() ([]byte, error) { data, err := action.ToInterface() if err != nil { return nil, err } return json.Marshal(data) } func (action *IncidentAction) UnmarshalJSON(b []byte) error { var data interface{} err := json.Unmarshal(b, &data) if err != nil { return err } return action.ParseInterface(data) } func (action *IncidentAction) ParseInterface(v interface{}) error { m, ok := v.(map[string]interface{}) if !ok { return fmt.Errorf("can't parse %v from %T", v, v) } t, ok := m["type"].(string) if !ok { return errors.New("no type") } if t != IncidentActionType { return fmt.Errorf("got bad type: %v", t) } action.Title, ok = m["title"].(string) if !ok { return errors.New("no title") } // We can process without comment and description action.Comment, _ = m["comment"].(string) action.Description, _ = m["description"].(string) if w, ok := m["effects"]; ok { switch v := w.(type) { case []interface{}: { for _, cur := range v { switch w := cur.(type) { case string: action.Effects = append(action.Effects, w) case float64: action.Effects = append(action.Effects, fmt.Sprintf("%v", w)) default: return fmt.Errorf("bad effect type: %T with value %v in interface %v", cur, cur, m) } } } case string: action.Effects = append(action.Effects, v) case float64: action.Effects = append(action.Effects, fmt.Sprintf("%v", v)) default: return fmt.Errorf("bad effect type: %T with value %v in interface %v", v, v, m) } } action.Category, _ = m["category"].(string) action.Importance, ok = m["importance"].(string) if !ok { return errors.New("no importance") } action.Assigned, _ = m["assigned_to"].(string) if w, ok := m["close_recommendations"]; ok { switch v := w.(type) { case []interface{}: { for _, cur := range v { switch w := cur.(type) { case string: action.CloseRecommendations = append(action.CloseRecommendations, w) case float64: action.CloseRecommendations = append(action.CloseRecommendations, fmt.Sprintf("%v", w)) case nil: break default: return fmt.Errorf("bad recommendations type: %T with value %v in interface %v", cur, cur, m) } } } case string: action.CloseRecommendations = append(action.CloseRecommendations, v) case float64: action.CloseRecommendations = append(action.CloseRecommendations, fmt.Sprintf("%v", v)) default: return fmt.Errorf("bad close recommendations type: %T with value %v in interface %v", v, v, m) } } return nil } func (action *IncidentAction) Perform(events *[]*events.Event) error { cl := log.WithFields(log.Fields{"type": IncidentActionType, "event_count": len(*events)}) cl.Debug("Start action") defer cl.Debug("End action") if events == nil || len(*events) == 0 { cl.Error("No events") return nil } var err error // Check if we need to prepare templates if action.template.title == nil { action.template.title, err = template.New("Title").Parse(action.Title) if err != nil { cl.Errorf("Can't create title template: %v", err) return err } action.template.comment, err = template.New("Comment").Parse(action.Comment) if err != nil { cl.Errorf("Can't create comment template: %v", err) return err } action.template.description, err = template.New("Description").Parse(action.Description) if err != nil { cl.Errorf("Can't create description template: %v", err) return err } } // Check if we need to prepare http client if action.client == nil { if viper.GetBool(config.ConsoleIgnoreSSLErrors) { transport := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } action.client = &http.Client{Transport: transport} } else { action.client = &http.Client{} } } // Get auth token if action.token == "" { action.token, err = ObtainAuthToken() if err != nil { cl.Errorf("Can't get auth token: %v", err) return err } } // For this type of action, multi means that we need to add all events // to created incident // Prepare body ibody, err := action.ToInterface() if err != nil { return err } cur := *events event := cur[0] // Render templates if _, ok := ibody["title"]; ok { s, err := renderTemplate(action.template.title, event) if err != nil { return err } // Need to truncate title, so serializer will accept it if len(s) > 256 { s = s[:255] } ibody["title"] = s } if _, ok := ibody["comment"]; ok { ibody["comment"], err = renderTemplate(action.template.comment, event) if err != nil { return err } } if _, ok := ibody["description"]; ok { ibody["description"], err = renderTemplate(action.template.description, event) if err != nil { return err } } // Add type to connect incident with sensor ibody["sensor"] = event.GetString("type") // Insert incidents info // If we have a single-event rule, len of events will be 1 ibody["event_count"] = len(*events) ibody["events"] = events jsonBody, err := json.Marshal(ibody) if err != nil { cl.Errorf("Can't serialize body: %v", err) return err } request, err := http.NewRequest("POST", viper.GetString(config.ConsoleUrlIncident), bytes.NewBuffer(jsonBody)) if err != nil { cl.Errorf("Can't create request: %v", err) return err } // Set headers request.Header.Set("Authorization", fmt.Sprintf("Token %v", action.token)) request.Header.Set("Content-type", "application/json") // Do request response, err := action.client.Do(request) if err != nil { cl.Errorf("Can't send request: %v", err) return err } defer response.Body.Close() // Check result body, err := ioutil.ReadAll(response.Body) if err != nil { cl.Errorf("Can't read response body: %v", err) return err } // Dump request and response if viper.GetBool(config.DebugDumpRequest) { go DumpNetwork("incident", *request.URL, ibody, body, response.StatusCode) } if response.StatusCode != http.StatusCreated { cl.Errorf("Bad status code, expect %v, got %v", http.StatusCreated, response.Status) return fmt.Errorf("bad status code, expect %v, got %v", http.StatusCreated, response.Status) } return nil }