// getHeartbeat returns a 200 func getHeartbeat(respWriter http.ResponseWriter, request *http.Request) { opid := mig.GenID() loc := fmt.Sprintf("%s%s", ctx.Server.Host, request.URL.String()) resource := cljs.New(loc) defer func() { if e := recover(); e != nil { ctx.Channels.Log <- mig.Log{OpID: opid, Desc: fmt.Sprintf("%v", e)}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%.0f", opid), Message: fmt.Sprintf("%v", e)}) respond(500, resource, respWriter, request) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving getHeartbeat()"}.Debug() }() err := resource.AddItem(cljs.Item{ Href: request.URL.String(), Data: []cljs.Data{ { Name: "heartbeat", Value: "gatorz say hi", }, }}) if err != nil { panic(err) } respond(200, resource, respWriter, request) }
func createCommand(ctx Context, action mig.Action, target mig.KeepAlive) (cmdid uint64, err error) { defer func() { if e := recover(); e != nil { err = fmt.Errorf("createCommand() -> %v", e) } ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: action.ID, CommandID: cmdid, Desc: "leaving createCommand()"}.Debug() }() cmdid = mig.GenID() cmd := mig.Command{ AgentName: target.Name, AgentQueueLoc: target.QueueLoc, Status: "prepared", Action: action, ID: cmdid, StartTime: time.Now().UTC(), } data, err := json.Marshal(cmd) if err != nil { panic(err) } dest := fmt.Sprintf("%s/%d-%d.json", ctx.Directories.Command.Ready, action.ID, cmdid) err = safeWrite(ctx, dest, data) if err != nil { panic(err) } desc := fmt.Sprintf("created command for action '%s' on target '%s'", action.Name, target.Name) ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: action.ID, CommandID: cmdid, Desc: desc} return }
// getCommand takes an actionid and a commandid and returns a command func getCommand(respWriter http.ResponseWriter, request *http.Request) { var err error opid := mig.GenID() resource := cljs.New(request.URL.String()) defer func() { if e := recover(); e != nil { ctx.Channels.Log <- mig.Log{OpID: opid, Desc: fmt.Sprintf("%v", e)}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%d", opid), Message: fmt.Sprintf("%v", e)}) respond(500, resource, respWriter, request, opid) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving getCommand()"}.Debug() }() var actionID, commandID int aid := request.URL.Query()["actionid"][0] if aid != "" { actionID, err = strconv.Atoi(aid) if err != nil { panic(err) } } cmdid := request.URL.Query()["commandid"][0] if cmdid != "" { commandID, err = strconv.Atoi(cmdid) if err != nil { panic(err) } } // retrieve the action cmds := []mig.Command{} var iter *mgo.Iter if commandID > 0 { if actionID > 0 { iter = ctx.DB.Col.Cmd.Find(bson.M{"id": commandID, "action.id": actionID}).Iter() } else { iter = ctx.DB.Col.Cmd.Find(bson.M{"id": commandID}).Iter() } } else { // nothing to search for, return empty resource respond(200, resource, respWriter, request, opid) } err = iter.All(&cmds) if err != nil { panic(err) } if len(cmds) == 0 { resource.SetError(cljs.Error{Code: fmt.Sprintf("%d", opid), Message: "No command found"}) respond(404, resource, respWriter, request, opid) } // store the results in the resource for _, cmd := range cmds { commandItem, err := commandToItem(cmd) if err != nil { panic(err) } resource.AddItem(commandItem) } respond(200, resource, respWriter, request, opid) }
// issueKillAction issues an `agentdestroy` action targetted to a specific agent // and updates the status of the agent in the database func issueKillAction(agent mig.Agent, ctx Context) (err error) { defer func() { if e := recover(); e != nil { err = fmt.Errorf("issueKillAction() -> %v", e) } ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, Desc: "leaving issueKillAction()"}.Debug() }() // generate an `agentdestroy` action for this agent killAction := mig.Action{ ID: mig.GenID(), Name: fmt.Sprintf("Kill agent %s", agent.Name), Target: fmt.Sprintf("queueloc='%s'", agent.QueueLoc), ValidFrom: time.Now().Add(-60 * time.Second).UTC(), ExpireAfter: time.Now().Add(30 * time.Minute).UTC(), SyntaxVersion: 2, } var opparams struct { PID int `json:"pid"` Version string `json:"version"` } opparams.PID = agent.PID opparams.Version = agent.Version killOperation := mig.Operation{ Module: "agentdestroy", Parameters: opparams, } killAction.Operations = append(killAction.Operations, killOperation) // sign the action with the scheduler PGP key secring, err := getSecring(ctx) if err != nil { panic(err) } pgpsig, err := killAction.Sign(ctx.PGP.PrivKeyID, secring) if err != nil { panic(err) } killAction.PGPSignatures = append(killAction.PGPSignatures, pgpsig) var jsonAction []byte jsonAction, err = json.Marshal(killAction) if err != nil { panic(err) } // write the action to the spool for scheduling dest := fmt.Sprintf("%s/%.0f.json", ctx.Directories.Action.New, killAction.ID) err = safeWrite(ctx, dest, jsonAction) if err != nil { panic(err) } // mark the agent as `destroyed` in the database err = ctx.DB.MarkAgentDestroyed(agent) if err != nil { panic(err) } ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf("issued kill action for agent '%s' with PID '%d'", agent.Name, agent.PID)}.Warning() return }
// getAction queries the database and retrieves the detail of an action func getAction(respWriter http.ResponseWriter, request *http.Request) { var err error opid := mig.GenID() loc := fmt.Sprintf("http://%s:%d%s", ctx.Server.IP, ctx.Server.Port, request.URL.String()) resource := cljs.New(loc) defer func() { if e := recover(); e != nil { emsg := fmt.Sprintf("%v", e) ctx.Channels.Log <- mig.Log{OpID: opid, Desc: emsg}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%.0f", opid), Message: emsg}) respond(500, resource, respWriter, request, opid) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving getAction()"}.Debug() }() actionID, err := strconv.ParseFloat(request.URL.Query()["actionid"][0], 64) if err != nil { err = fmt.Errorf("Wrong parameters 'actionid': '%v'", err) panic(err) } // retrieve the action var a mig.Action if actionID > 0 { a, err = ctx.DB.ActionByID(actionID) if err != nil { if a.ID == -1 { // not found, return 404 resource.SetError(cljs.Error{ Code: fmt.Sprintf("%.0f", opid), Message: fmt.Sprintf("Action ID '%.0f' not found", actionID)}) respond(404, resource, respWriter, request, opid) return } else { panic(err) } } } else { // bad request, return 400 resource.SetError(cljs.Error{ Code: fmt.Sprintf("%.0f", opid), Message: fmt.Sprintf("Invalid Action ID '%.0f'", actionID)}) respond(400, resource, respWriter, request, opid) return } // retrieve investigators a.Investigators, err = ctx.DB.InvestigatorByActionID(a.ID) if err != nil { panic(err) } // store the results in the resource actionItem, err := actionToItem(a, true, ctx) if err != nil { panic(err) } resource.AddItem(actionItem) respond(200, resource, respWriter, request, opid) }
// processNewAction is called when a new action is available. It pulls // the action from the directory, parse it, retrieve a list of targets from // the backend database, and create individual command for each target. func processNewAction(actionPath string, ctx Context) (err error) { var ea mig.ExtendedAction defer func() { if e := recover(); e != nil { err = fmt.Errorf("processNewAction() -> %v", e) } ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: ea.Action.ID, Desc: "leaving processNewAction()"}.Debug() }() // load the action file a, err := mig.ActionFromFile(actionPath) if err != nil { panic(err) } ea.Action = a ea.StartTime = time.Now() // generate an action id if ea.Action.ID < 1 { ea.Action.ID = mig.GenID() } desc := fmt.Sprintf("new action received: Name='%s' Target='%s' ValidFrom='%s' ExpireAfter='%s'", ea.Action.Name, ea.Action.Target, ea.Action.ValidFrom, ea.Action.ExpireAfter) ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: ea.Action.ID, Desc: desc} // TODO: replace with action.Validate(), to include signature verification if time.Now().Before(ea.Action.ValidFrom) { // queue new action ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: ea.Action.ID, Desc: fmt.Sprintf("action '%s' not ready for scheduling", ea.Action.Name)} return } if time.Now().After(ea.Action.ExpireAfter) { ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: ea.Action.ID, Desc: fmt.Sprintf("removing expired action '%s'", ea.Action.Name)} // delete expired action os.Remove(actionPath) return } // expand the action in one command per agent ea.CommandIDs, err = prepareCommands(ea.Action, ctx) if err != nil { panic(err) } // move action to Fly-ing state ea.Status = "inflight" ea.Counters.Sent = len(ea.CommandIDs) err = flyAction(ctx, ea, actionPath) if err != nil { panic(err) } return }
// startAgentsListener will create an AMQP consumer for this agent if none exist func startAgentListener(reg mig.KeepAlive, ctx Context) (err error) { defer func() { if e := recover(); e != nil { err = fmt.Errorf("startAgentListener() -> %v", e) } ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, Desc: "leaving startAgentListener()"}.Debug() }() // If a listener already exists for this agent, exit for _, q := range activeAgentsList { if q == reg.QueueLoc { desc := fmt.Sprintf("startAgentListener() already has a listener for '%s'", reg.QueueLoc) ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, Desc: desc}.Debug() return } } //create a queue for agent queue := fmt.Sprintf("mig.sched.%s", reg.QueueLoc) _, err = ctx.MQ.Chan.QueueDeclare(queue, true, false, false, false, nil) if err != nil { panic(err) } err = ctx.MQ.Chan.QueueBind(queue, queue, "mig", false, nil) if err != nil { panic(err) } agentChan, err := ctx.MQ.Chan.Consume(queue, "", true, false, false, false, nil) if err != nil { panic(err) } // start a goroutine for this queue go func() { for msg := range agentChan { ctx.OpID = mig.GenID() err = recvAgentResults(msg, ctx) if err != nil { ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, Desc: fmt.Sprintf("%v", err)}.Err() // TODO: agent is sending bogus results, do something about it } } }() desc := fmt.Sprintf("startAgentactiveAgentsListener: started recvAgentResults goroutine for agent '%s'", reg.Name) ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, Desc: desc}.Debug() // add the new active queue to the activeAgentsList activeAgentsList = append(activeAgentsList, reg.QueueLoc) return }
func getDashboard(respWriter http.ResponseWriter, request *http.Request) { opid := mig.GenID() loc := fmt.Sprintf("http://%s:%d%s", ctx.Server.IP, ctx.Server.Port, request.URL.String()) resource := cljs.New(loc) defer func() { if e := recover(); e != nil { ctx.Channels.Log <- mig.Log{OpID: opid, Desc: fmt.Sprintf("%v", e)}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%.0f", opid), Message: fmt.Sprintf("%v", e)}) respond(500, resource, respWriter, request, opid) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving getDashboard()"}.Debug() }() // get summary of agents active in the last 5 minutes sum, err := ctx.DB.SumAgentsByVersion(time.Now().Add(-5 * time.Minute)) if err != nil { panic(err) } count, err := ctx.DB.CountNewAgents(time.Now().Add(-24 * time.Hour)) if err != nil { panic(err) } double, err := ctx.DB.CountDoubleAgents(time.Now().Add(-5 * time.Minute)) if err != nil { panic(err) } disappeared, err := ctx.DB.CountDisappearedAgents( time.Now().Add(-7*24*time.Hour), time.Now().Add(-5*time.Minute)) if err != nil { panic(err) } sumItem, err := agentsSummaryToItem(sum, count, double, disappeared, ctx) resource.AddItem(sumItem) // add the last 10 actions actions, err := ctx.DB.Last10Actions() if err != nil { panic(err) } for _, action := range actions { // retrieve investigators action.Investigators, err = ctx.DB.InvestigatorByActionID(action.ID) if err != nil { panic(err) } // store the results in the resource actionItem, err := actionToItem(action, false, ctx) if err != nil { panic(err) } resource.AddItem(actionItem) } respond(200, resource, respWriter, request, opid) }
// getIP returns a the public IP of the caller as read from X-Forwarded-For func getIP(respWriter http.ResponseWriter, request *http.Request) { opid := mig.GenID() defer func() { ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving getIP()"}.Debug() }() if request.Header.Get("X-FORWARDED-FOR") != "" { respond(200, []byte(request.Header.Get("X-FORWARDED-FOR")), respWriter, request) } else { // request.RemoteAddr contains IP:Port, so strip the port and return just the IP respond(200, []byte(request.RemoteAddr[:strings.LastIndex(request.RemoteAddr, ":")]), respWriter, request) } }
// getCommand takes an actionid and a commandid and returns a command func getCommand(respWriter http.ResponseWriter, request *http.Request) { var err error opid := mig.GenID() loc := fmt.Sprintf("http://%s:%d%s", ctx.Server.IP, ctx.Server.Port, request.URL.String()) resource := cljs.New(loc) defer func() { if e := recover(); e != nil { emsg := fmt.Sprintf("%v", e) ctx.Channels.Log <- mig.Log{OpID: opid, Desc: emsg}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%.0f", opid), Message: emsg}) respond(500, resource, respWriter, request, opid) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving getCommand()"}.Debug() }() commandID, err := strconv.ParseFloat(request.URL.Query()["commandid"][0], 64) if err != nil { err = fmt.Errorf("Wrong parameters 'commandid': '%v'", err) panic(err) } // retrieve the command var cmd mig.Command if commandID > 0 { cmd, err = ctx.DB.CommandByID(commandID) if err != nil { if fmt.Sprintf("%v", err) == "Error while retrieving command: 'sql: no rows in result set'" { // not found, return 404 resource.SetError(cljs.Error{ Code: fmt.Sprintf("%.0f", opid), Message: fmt.Sprintf("Command ID '%.0f' not found", commandID)}) respond(404, resource, respWriter, request, opid) return } else { panic(err) } } } else { // bad request, return 400 resource.SetError(cljs.Error{ Code: fmt.Sprintf("%.0f", opid), Message: fmt.Sprintf("Invalid Command ID '%.0f'", commandID)}) respond(400, resource, respWriter, request, opid) return } // store the results in the resource commandItem, err := commandToItem(cmd) if err != nil { panic(err) } resource.AddItem(commandItem) respond(200, resource, respWriter, request, opid) }
func searchAgents(respWriter http.ResponseWriter, request *http.Request) { opid := mig.GenID() resource := cljs.New(request.URL.String()) defer func() { if e := recover(); e != nil { ctx.Channels.Log <- mig.Log{OpID: opid, Desc: fmt.Sprintf("%v", e)}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%d", opid), Message: fmt.Sprintf("%v", e)}) respond(500, resource, respWriter, request, opid) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving searchAgents()"}.Debug() }() respond(501, resource, respWriter, request, opid) }
// cancelCommand receives an action ID and a command ID and issues a cancellation order func cancelCommand(respWriter http.ResponseWriter, request *http.Request) { opid := mig.GenID() loc := fmt.Sprintf("http://%s:%d%s", ctx.Server.IP, ctx.Server.Port, request.URL.String()) resource := cljs.New(loc) defer func() { if e := recover(); e != nil { ctx.Channels.Log <- mig.Log{OpID: opid, Desc: fmt.Sprintf("%v", e)}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%.0f", opid), Message: fmt.Sprintf("%v", e)}) respond(500, resource, respWriter, request, opid) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving cancelCommand()"}.Debug() }() respond(501, resource, respWriter, request, opid) }
func getAgent(respWriter http.ResponseWriter, request *http.Request) { opid := mig.GenID() loc := fmt.Sprintf("http://%s:%d%s", ctx.Server.IP, ctx.Server.Port, request.URL.String()) resource := cljs.New(loc) defer func() { if e := recover(); e != nil { ctx.Channels.Log <- mig.Log{OpID: opid, Desc: fmt.Sprintf("%v", e)}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%.0f", opid), Message: fmt.Sprintf("%v", e)}) respond(500, resource, respWriter, request, opid) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving getAgentsDashboard()"}.Debug() }() agentID, err := strconv.ParseFloat(request.URL.Query()["agentid"][0], 64) if err != nil { err = fmt.Errorf("Wrong parameters 'agentid': '%v'", err) panic(err) } // retrieve the command var agt mig.Agent if agentID > 0 { agt, err = ctx.DB.AgentByID(agentID) if err != nil { if fmt.Sprintf("%v", err) == "Error while retrieving agent: 'sql: no rows in result set'" { // not found, return 404 resource.SetError(cljs.Error{ Code: fmt.Sprintf("%.0f", opid), Message: fmt.Sprintf("Agent ID '%.0f' not found", agentID)}) respond(404, resource, respWriter, request, opid) return } else { panic(err) } } } else { // bad request, return 400 resource.SetError(cljs.Error{ Code: fmt.Sprintf("%.0f", opid), Message: fmt.Sprintf("Invalid Agent ID '%.0f'", agentID)}) respond(400, resource, respWriter, request, opid) return } // store the results in the resource agentItem, err := agentToItem(agt) if err != nil { panic(err) } resource.AddItem(agentItem) respond(200, resource, respWriter, request, opid) }
// safeWrite performs a two steps write: // 1) a temp file is written // 2) the temp file is moved into the target folder // this prevents the dir watcher from waking up before the file is fully written func safeWrite(ctx Context, destination string, data []byte) (err error) { if len(data) == 0 { return fmt.Errorf("data slice is empty. file not written") } // write the file temp dir tmp := fmt.Sprintf("%s/%.0f", ctx.Directories.Tmp, mig.GenID()) err = ioutil.WriteFile(tmp, data, 0640) if err != nil { return fmt.Errorf("safeWrite: %v", err) } // move to destination err = os.Rename(tmp, destination) if err != nil { return fmt.Errorf("safeWrite: %v", err) } return }
// recvAgentResults listens on the AMQP channel for command results from agents // each iteration processes one command received from one agent. The json.Body // in the message is extracted and written into ctx.Directories.Command.Done func recvAgentResults(msg amqp.Delivery, ctx Context) (err error) { defer func() { if e := recover(); e != nil { err = fmt.Errorf("recvAgentResults() -> %v", e) } ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, Desc: "leaving recvAgentResults()"}.Debug() }() // write to disk Returned directory dest := fmt.Sprintf("%s/%d", ctx.Directories.Command.Returned, mig.GenID()) err = safeWrite(ctx, dest, msg.Body) if err != nil { panic(err) } ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, Desc: fmt.Sprintf("Received result from '%s'", msg.RoutingKey)}.Debug() return }
func createCommand(ctx Context, action mig.Action, agent mig.Agent, emptyResults []modules.Result) (err error) { cmdid := mig.GenID() defer func() { if e := recover(); e != nil { err = fmt.Errorf("createCommand() -> %v", e) } ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: action.ID, CommandID: cmdid, Desc: "leaving createCommand()"}.Debug() }() var cmd mig.Command cmd.Status = "sent" cmd.Action = action cmd.Agent = agent cmd.ID = cmdid cmd.StartTime = time.Now().UTC() cmd.Results = emptyResults ctx.Channels.CommandReady <- cmd return }
// safeWrite performs a two steps write: // 1) a temp file is written // 2) the temp file is moved into the target folder // this prevents the dir watcher from waking up before the file is fully written func safeWrite(ctx Context, destination string, data []byte) (err error) { defer func() { if e := recover(); e != nil { err = fmt.Errorf("safeWrite() -> %v", e) } ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, Desc: "leaving safeWrite()"}.Debug() }() // write the file temp dir tmp := fmt.Sprintf("%s/%.0f", ctx.Directories.Tmp, mig.GenID()) err = ioutil.WriteFile(tmp, data, 0640) if err != nil { panic(err) } // move to destination err = os.Rename(tmp, destination) if err != nil { panic(err) } return }
func createCommand(ctx Context, action mig.Action, agent mig.Agent) (err error) { cmdid := mig.GenID() defer func() { if e := recover(); e != nil { err = fmt.Errorf("createCommand() -> %v", e) } ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: action.ID, CommandID: cmdid, Desc: "leaving createCommand()"}.Debug() }() cmd := mig.Command{ Status: "sent", Action: action, Agent: agent, ID: cmdid, StartTime: time.Now().UTC(), } cmd.Agent.Name = agent.Name cmd.Agent.QueueLoc = agent.QueueLoc ctx.Channels.CommandReady <- cmd desc := fmt.Sprintf("created command for action '%s' on agent '%s'", action.Name, agent.Name) ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: action.ID, CommandID: cmdid, Desc: desc} return }
// describeCancelCommand returns a resource that describes how to cancel a command func describeCancelCommand(respWriter http.ResponseWriter, request *http.Request) { opid := mig.GenID() resource := cljs.New(request.URL.String()) defer func() { if e := recover(); e != nil { ctx.Channels.Log <- mig.Log{OpID: opid, Desc: fmt.Sprintf("%v", e)}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%d", opid), Message: fmt.Sprintf("%v", e)}) respond(500, resource, respWriter, request, opid) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving describeCancelCommand()"}.Debug() }() err := resource.SetTemplate(cljs.Template{ Data: []cljs.Data{ {Name: "actionid", Value: "[0-9]{1,20}", Prompt: "Action ID"}, {Name: "commandid", Value: "[0-9]{1,20}", Prompt: "Command ID"}, }, }) if err != nil { panic(err) } respond(200, resource, respWriter, request, opid) }
// getAction queries the database and retrieves the detail of an action func getAction(respWriter http.ResponseWriter, request *http.Request) { var err error opid := mig.GenID() resource := cljs.New(request.URL.String()) defer func() { if e := recover(); e != nil { ctx.Channels.Log <- mig.Log{OpID: opid, Desc: fmt.Sprintf("%v", e)}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%d", opid), Message: fmt.Sprintf("%v", e)}) respond(500, resource, respWriter, request, opid) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving getAction()"}.Debug() }() actionID, err := strconv.Atoi(request.URL.Query()["actionid"][0]) if err != nil { panic(err) } // retrieve the action eas := []mig.ExtendedAction{} iter := ctx.DB.Col.Action.Find(bson.M{"action.id": actionID}).Iter() err = iter.All(&eas) if err != nil { panic(err) } if len(eas) == 0 { resource.SetError(cljs.Error{Code: fmt.Sprintf("%d", opid), Message: "Action not found"}) respond(404, resource, respWriter, request, opid) } // store the results in the resource for _, ea := range eas { actionItem, err := extendedActionToItem(ea) if err != nil { panic(err) } resource.AddItem(actionItem) } respond(200, resource, respWriter, request, opid) }
// describeCreateAction returns a resource that describes how to POST new actions func describeCreateAction(respWriter http.ResponseWriter, request *http.Request) { opid := mig.GenID() resource := cljs.New(request.URL.Path) defer func() { if e := recover(); e != nil { ctx.Channels.Log <- mig.Log{OpID: opid, Desc: fmt.Sprintf("%v", e)}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%d", opid), Message: fmt.Sprintf("%v", e)}) respond(500, resource, respWriter, request, opid) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving describeCreateAction()"}.Debug() }() err := resource.SetTemplate(cljs.Template{ Data: []cljs.Data{ {Name: "action", Value: "", Prompt: "Signed MIG Action"}, }, }) if err != nil { panic(err) } respond(200, resource, respWriter, request, opid) }
// InsertAgent creates a new agent in the database func (db *DB) InsertAgent(agt mig.Agent) (err error) { jEnv, err := json.Marshal(agt.Env) if err != nil { err = fmt.Errorf("Failed to marshal agent environment: '%v'", err) return } jTags, err := json.Marshal(agt.Tags) if err != nil { err = fmt.Errorf("Failed to marshal agent tags: '%v'", err) return } agtid := mig.GenID() _, err = db.c.Exec(`INSERT INTO agents (id, name, queueloc, os, version, pid, starttime, destructiontime, heartbeattime, status, environment, tags) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)`, agtid, agt.Name, agt.QueueLoc, agt.OS, agt.Version, agt.PID, agt.StartTime, agt.DestructionTime, agt.HeartBeatTS, agt.Status, jEnv, jTags) if err != nil { return fmt.Errorf("Failed to insert agent in database: '%v'", err) } return }
// search is a generic function to run queries against mongodb func search(respWriter http.ResponseWriter, request *http.Request) { opid := mig.GenID() resource := cljs.New(request.URL.String()) defer func() { if e := recover(); e != nil { ctx.Channels.Log <- mig.Log{OpID: opid, Desc: fmt.Sprintf("%v", e)}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%d", opid), Message: fmt.Sprintf("%v", e)}) respond(500, resource, respWriter, request, opid) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving describeCreateAction()"}.Debug() }() search := request.URL.Query()["search"][0] switch search { case "positiveresults": actionID, err := strconv.Atoi(request.URL.Query()["actionid"][0]) if err != nil { panic(err) } cmds, err := findPositiveResults(actionID) if err != nil { panic(err) } // store the results in the resource for _, cmd := range cmds { commandItem, err := commandToItem(cmd) if err != nil { panic(err) } resource.AddItem(commandItem) } respond(200, resource, respWriter, request, opid) default: panic("unknown search method") } }
// getOpID returns an operation ID from a request context, and if not found, generates one func getOpID(r *http.Request) float64 { if opid := context.Get(r, opID); opid != nil { return opid.(float64) } return mig.GenID() }
// MakeSignedToken encrypts a timestamp and a random number with the users GPG key // to use as an auth token with the API func (cli Client) MakeSignedToken() (token string, err error) { defer func() { if e := recover(); e != nil { err = fmt.Errorf("MakeSignedToken() -> %v", e) } }() tokenVersion := 1 str := fmt.Sprintf("%d;%s;%.0f", tokenVersion, time.Now().UTC().Format(time.RFC3339), mig.GenID()) secringFile, err := os.Open(cli.Conf.GPG.Home + "/secring.gpg") if err != nil { panic(err) } defer secringFile.Close() sig, err := pgp.Sign(str+"\n", cli.Conf.GPG.KeyID, secringFile) if err != nil { panic(err) } token = str + ";" + sig return }
// createAction receives a signed action in a POST request, validates it, // and write it into the scheduler spool func createAction(respWriter http.ResponseWriter, request *http.Request) { var err error opid := mig.GenID() var action mig.Action resource := cljs.New(request.URL.String()) defer func() { if e := recover(); e != nil { ctx.Channels.Log <- mig.Log{OpID: opid, ActionID: action.ID, Desc: fmt.Sprintf("%v", e)}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%d", opid), Message: fmt.Sprintf("%v", e)}) respond(500, resource, respWriter, request, opid) } ctx.Channels.Log <- mig.Log{OpID: opid, ActionID: action.ID, Desc: "leaving createAction()"}.Debug() }() // parse the POST body into a mig action request.ParseForm() postAction := request.FormValue("action") err = json.Unmarshal([]byte(postAction), &action) if err != nil { panic(err) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: fmt.Sprintf("Received action for creation '%s'", action)}.Debug() // load keyring and validate action keyring, err := os.Open(ctx.OpenPGP.PubRing) if err != nil { panic(err) } defer keyring.Close() err = action.Validate() if err != nil { panic(err) } err = action.VerifySignature(keyring) if err != nil { panic(err) } action.ID = mig.GenID() ctx.Channels.Log <- mig.Log{OpID: opid, ActionID: action.ID, Desc: "Received new action with valid signature"} // write action to disk destdir := fmt.Sprintf("%s/%d.json", ctx.Directories.Action.New, action.ID) newAction, err := json.Marshal(action) if err != nil { panic(err) } err = safeWrite(opid, destdir, newAction) if err != nil { panic(err) } ctx.Channels.Log <- mig.Log{OpID: opid, ActionID: action.ID, Desc: "Action committed to spool"} err = resource.AddItem(cljs.Item{ Href: "/api/action?actionid=" + fmt.Sprintf("%d", action.ID), Data: []cljs.Data{{Name: "action ID " + fmt.Sprintf("%d", action.ID), Value: action}}, }) if err != nil { panic(err) } respond(201, resource, respWriter, request, opid) }
// getHome returns a basic document that presents the different ressources // available in the API, as well as some status information func getHome(respWriter http.ResponseWriter, request *http.Request) { var err error opid := mig.GenID() resource := cljs.New(request.URL.String()) defer func() { if e := recover(); e != nil { ctx.Channels.Log <- mig.Log{OpID: opid, Desc: fmt.Sprintf("%v", e)}.Err() resource.SetError(cljs.Error{Code: fmt.Sprintf("%d", opid), Message: fmt.Sprintf("%v", e)}) respond(500, resource, respWriter, request, opid) } ctx.Channels.Log <- mig.Log{OpID: opid, Desc: "leaving getHome()"}.Debug() }() // List the creation URL. Those can be GET-ed to retrieve the creation templates err = resource.AddLink(cljs.Link{ Rel: "create action", Href: "/api/action/create/", Name: "Create an action"}) if err != nil { panic(err) } err = resource.AddLink(cljs.Link{ Rel: "cancel action", Href: "/api/action/cancel/", Name: "Cancel an action"}) if err != nil { panic(err) } err = resource.AddLink(cljs.Link{ Rel: "cancel command", Href: "/api/command/cancel/", Name: "Cancel a command"}) if err != nil { panic(err) } // Describe the queries that are exposed to the client err = resource.AddQuery(cljs.Query{ Rel: "Query action by ID", Href: "/api/action", Prompt: "Query action by ID", Data: []cljs.Data{ {Name: "actionid", Value: "[0-9]{1,20}", Prompt: "Action ID"}, }, }) if err != nil { panic(err) } resource.AddQuery(cljs.Query{ Rel: "Query command by ID", Href: "/api/command", Prompt: "Query command by ID", Data: []cljs.Data{ {Name: "commandid", Value: "[0-9]{1,20}", Prompt: "Command ID"}, {Name: "actionid", Value: "[0-9]{1,20}", Prompt: "Action ID"}, }, }) if err != nil { panic(err) } resource.AddQuery(cljs.Query{ Rel: "Search agent by name", Href: "/api/agent/search", Prompt: "Search agent by name", Data: []cljs.Data{ {Name: "name", Value: "agent123.example.net", Prompt: "Agent Name"}, }, }) if err != nil { panic(err) } err = resource.AddQuery(cljs.Query{ Rel: "Query MIG data", Href: "/api/search", Prompt: "Query MIG data", Data: []cljs.Data{ {Name: "actionid", Value: "[0-9]{1,20}", Prompt: "Action ID"}, {Name: "search", Value: "positiveresults, ...", Prompt: "Name of search query"}, }, }) if err != nil { panic(err) } resource.AddQuery(cljs.Query{ Rel: "Get agent dashboard", Href: "/api/agent/dashboard", }) if err != nil { panic(err) } respond(200, resource, respWriter, request, opid) }
// processNewAction is called when a new action is available. It pulls // the action from the directory, parse it, retrieve a list of targets from // the backend database, and create individual command for each target. func processNewAction(actionPath string, ctx Context) (err error) { var action mig.Action defer func() { if e := recover(); e != nil { err = fmt.Errorf("processNewAction() -> %v", e) } ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: action.ID, Desc: "leaving processNewAction()"}.Debug() }() // load the action file action, err = mig.ActionFromFile(actionPath) if err != nil { panic(err) } action.StartTime = time.Now() // generate an action id if action.ID < 1 { action.ID = mig.GenID() } desc := fmt.Sprintf("new action received: Name='%s' Target='%s' ValidFrom='%s' ExpireAfter='%s'", action.Name, action.Target, action.ValidFrom, action.ExpireAfter) ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: action.ID, Desc: desc} // TODO: replace with action.Validate(), to include signature verification if time.Now().Before(action.ValidFrom) { // queue new action desc := fmt.Sprintf("action '%s' is not ready for scheduling", action.Name) ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: action.ID, Desc: desc}.Debug() return } if time.Now().After(action.ExpireAfter) { ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: action.ID, Desc: fmt.Sprintf("action '%s' is expired. invalidating.", action.Name)} err = invalidAction(ctx, action, actionPath) if err != nil { panic(err) } return } // find target agents for the action agents, err := ctx.DB.ActiveAgentsByTarget(action.Target) if err != nil { panic(err) } action.Counters.Sent = len(agents) if action.Counters.Sent == 0 { err = fmt.Errorf("No agents found for target '%s'. invalidating action.", action.Target) err = invalidAction(ctx, action, actionPath) if err != nil { panic(err) } } ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: action.ID, Desc: fmt.Sprintf("Found %d target agents", action.Counters.Sent)} action.Status = "preparing" inserted, err := ctx.DB.InsertOrUpdateAction(action) if err != nil { panic(err) } if inserted { // action was inserted, and not updated, so we need to insert // the signatures as well astr, err := action.String() if err != nil { panic(err) } for _, sig := range action.PGPSignatures { pubring, err := getPubring(ctx) if err != nil { panic(err) } fp, err := pgp.GetFingerprintFromSignature(astr, sig, pubring) if err != nil { panic(err) } inv, err := ctx.DB.InvestigatorByFingerprint(fp) if err != nil { panic(err) } err = ctx.DB.InsertSignature(action.ID, inv.ID, sig) if err != nil { panic(err) } } } ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: action.ID, Desc: "Action written to database"}.Debug() // create an array of empty results to serve as default for all commands emptyResults := make([]modules.Result, len(action.Operations)) created := 0 for _, agent := range agents { err := createCommand(ctx, action, agent, emptyResults) if err != nil { ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: action.ID, Desc: "Failed to create commmand on agent" + agent.Name}.Err() continue } created++ } if created == 0 { // no command created found ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, ActionID: action.ID, Desc: "No command created. Invalidating action."}.Err() err = invalidAction(ctx, action, actionPath) if err != nil { panic(err) } return nil } // move action to flying state err = flyAction(ctx, action, actionPath) if err != nil { panic(err) } return }
// parseCommands transforms a message into a MIG Command struct, performs validation // and run the command func parseCommands(ctx Context, msg []byte) (err error) { var cmd mig.Command cmd.ID = 0 // safety net defer func() { if e := recover(); e != nil { err = fmt.Errorf("parseCommands() -> %v", e) // if we have a command to return, update status and send back if cmd.ID > 0 { errLog := mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: fmt.Sprintf("%v", err)}.Err() cmd.Results = append(cmd.Results, errLog) cmd.Status = "failed" ctx.Channels.Results <- cmd } } ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: "leaving parseCommands()"}.Debug() }() // unmarshal the received command into a command struct // if this fails, inform the scheduler and skip this message err = json.Unmarshal(msg, &cmd) if err != nil { panic(err) } // get an io.Reader from the public pgp key keyring, keycount, err := pgp.ArmoredPubKeysToKeyring(PUBLICPGPKEYS[0:]) if err != nil { panic(err) } ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: fmt.Sprintf("loaded %d keys", keycount)}.Debug() // Check the action syntax and signature err = cmd.Action.Validate() if err != nil { desc := fmt.Sprintf("action validation failed: %v", err) ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: desc}.Err() panic(desc) } err = cmd.Action.VerifySignature(keyring) if err != nil { desc := fmt.Sprintf("action signature verification failed: %v", err) ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: desc}.Err() panic(desc) } // Expiration is verified by the Validate() call above, but we need // to verify the ScheduledDate ourselves if time.Now().Before(cmd.Action.ValidFrom) { ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: "action is scheduled for later"}.Err() panic("ScheduledDateInFuture") } // Each operation is ran separately by a module, a channel is created to receive the results from each module // a goroutine is created to read from the result channel, and when all modules are done, build the response resultChan := make(chan moduleResult) opsCounter := 0 for counter, operation := range cmd.Action.Operations { // create an module operation object currentOp := moduleOp{id: mig.GenID(), mode: operation.Module, params: operation.Parameters, resultChan: resultChan} desc := fmt.Sprintf("sending operation %d to module %s", counter, operation.Module) ctx.Channels.Log <- mig.Log{OpID: currentOp.id, ActionID: cmd.Action.ID, CommandID: cmd.ID, Desc: desc} // pass the module operation object to the proper channel switch operation.Module { case "filechecker": // send the operation to the module ctx.Channels.RunAgentCommand <- currentOp opsCounter++ case "shell": // send to the external execution path ctx.Channels.RunExternalCommand <- currentOp opsCounter++ case "terminate": ctx.Channels.Terminate <- fmt.Errorf("Terminate order received from scheduler") opsCounter++ default: ctx.Channels.Log <- mig.Log{CommandID: cmd.ID, ActionID: cmd.Action.ID, Desc: fmt.Sprintf("module '%s' is invalid", operation.Module)} } } // start the goroutine that will receive the results go receiveModuleResults(ctx, cmd, resultChan, opsCounter) return }
// main initializes the mongodb connection, the directory watchers and the // AMQP ctx.MQ.Chan. It also launches the goroutines. func main() { // command line options var config = flag.String("c", "/etc/mig/scheduler.cfg", "Load configuration from file") flag.Parse() // The context initialization takes care of parsing the configuration, // and creating connections to database, message broker, syslog, ... fmt.Fprintf(os.Stderr, "Initializing Scheduler context...") ctx, err := Init(*config) if err != nil { panic(err) } fmt.Fprintf(os.Stderr, "OK\n") // Goroutine that handles events, such as logs and panics, // and decides what to do with them go func() { for event := range ctx.Channels.Log { stop, err := mig.ProcessLog(ctx.Logging, event) if err != nil { panic("Unable to process logs") } // if ProcessLog says we should stop now, feed the Terminate chan if stop { ctx.Channels.Terminate <- errors.New(event.Desc) } } }() ctx.Channels.Log <- mig.Log{Desc: "mig.ProcessLog() routine started"} // Watch the data directories for new files watcher, err := fsnotify.NewWatcher() if err != nil { panic(err) } go watchDirectories(watcher, ctx) err = initWatchers(watcher, ctx) if err != nil { panic(err) } // Goroutine that loads actions dropped into ctx.Directories.Action.New go func() { for actionPath := range ctx.Channels.NewAction { ctx.OpID = mig.GenID() err := processNewAction(actionPath, ctx) // if something fails in the action processing, move it to the invalid folder if err != nil { // move action to INVALID folder and log dest := fmt.Sprintf("%s/%d.json", ctx.Directories.Action.Invalid, time.Now().UTC().UnixNano()) os.Rename(actionPath, dest) reason := fmt.Sprintf("%v. %s moved to %s", err, actionPath, dest) ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, Desc: reason}.Warning() } } }() ctx.Channels.Log <- mig.Log{Desc: "processNewAction() routine started"} // Goroutine that loads and sends commands dropped into ctx.Directories.Command.Ready go func() { for cmdPath := range ctx.Channels.CommandReady { ctx.OpID = mig.GenID() err := sendCommand(cmdPath, ctx) if err != nil { ctx.Channels.Log <- mig.Log{OpID: ctx.OpID, Desc: fmt.Sprintf("%v", err)}.Err() } } }() ctx.Channels.Log <- mig.Log{Desc: "sendCommand() routine started"} // Goroutine that loads commands from the ctx.Directories.Command.Returned and marks // them as finished or cancelled go func() { for cmdPath := range ctx.Channels.CommandReturned { ctx.OpID = mig.GenID() err := terminateCommand(cmdPath, ctx) if err != nil { ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf("%v", err)}.Err() } } }() ctx.Channels.Log <- mig.Log{Desc: "terminateCommand() routine started"} // Goroutine that updates an action when a command is done go func() { for cmdPath := range ctx.Channels.CommandDone { ctx.OpID = mig.GenID() err = updateAction(cmdPath, ctx) if err != nil { ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf("%v", err)}.Err() } } }() ctx.Channels.Log <- mig.Log{Desc: "updateAction() routine started"} // Init is completed, restart queues of agents that have recent // keepalives in the database err = pickUpAliveAgents(ctx) if err != nil { panic(err) } // start a listening channel to receive keepalives from agents keepaliveChan, err := startKeepAliveChannel(ctx) if err != nil { panic(err) } // launch the routine that handles registrations go func() { for msg := range keepaliveChan { ctx.OpID = mig.GenID() err := getKeepAlives(msg, ctx) if err != nil { ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf("%v", err)}.Err() } } }() ctx.Channels.Log <- mig.Log{Desc: "Agent KeepAlive routine started"} // launch the routine that regularly walks through the local directories go func() { collectorSleeper, err := time.ParseDuration(ctx.Collector.Freq) if err != nil { panic(err) } for { ctx.OpID = mig.GenID() err := spoolInspection(ctx) if err != nil { ctx.Channels.Log <- mig.Log{Desc: fmt.Sprintf("%v", err)}.Err() } time.Sleep(collectorSleeper) } }() ctx.Channels.Log <- mig.Log{Desc: "spoolInspection() routine started"} // won't exit until this chan received something exitReason := <-ctx.Channels.Terminate fmt.Fprintf(os.Stderr, "Scheduler is shutting down. Reason: %s", exitReason) Destroy(ctx) }