mig/action.go

442 строки
13 KiB
Go

// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Contributor: Julien Vehent jvehent@mozilla.com [:ulfr]
package mig /* import "mig.ninja/mig" */
import (
"bytes"
"compress/gzip"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strconv"
"sync"
"time"
"mig.ninja/mig/pgp"
)
// ActionVersion is the version of the syntax that is expected
const ActionVersion uint16 = 2
// an Action is the json object that is created by an investigator
// and provided to the MIG platform. It must be PGP signed.
type Action struct {
ID float64 `json:"id"`
Name string `json:"name"`
Target string `json:"target"`
Description Description `json:"description,omitempty"`
Threat Threat `json:"threat,omitempty"`
ValidFrom time.Time `json:"validfrom"`
ExpireAfter time.Time `json:"expireafter"`
Operations []Operation `json:"operations"`
PGPSignatures []string `json:"pgpsignatures"`
Investigators []Investigator `json:"investigators,omitempty"`
Status string `json:"status,omitempty"`
StartTime time.Time `json:"starttime,omitempty"`
FinishTime time.Time `json:"finishtime,omitempty"`
LastUpdateTime time.Time `json:"lastupdatetime,omitempty"`
Counters ActionCounters `json:"counters,omitempty"`
SyntaxVersion uint16 `json:"syntaxversion,omitempty"`
}
// Some counters used to track the completion of an action
type ActionCounters struct {
Sent int `json:"sent,omitempty"`
Done int `json:"done,omitempty"`
InFlight int `json:"inflight,omitempty"`
Success int `json:"success,omitempty"`
Cancelled int `json:"cancelled,omitempty"`
Expired int `json:"expired,omitempty"`
Failed int `json:"failed,omitempty"`
TimeOut int `json:"timeout,omitempty"`
}
// a description is a simple object that contains detail about the
// action's author, and it's revision.
type Description struct {
Author string `json:"author,omitempty"`
Email string `json:"email,omitempty"`
URL string `json:"url,omitempty"`
Revision float64 `json:"revision,omitempty"`
}
// a threat provides the investigator with an idea of how dangerous
// a the compromission might be, if the indicators return positive
type Threat struct {
Ref string `json:"ref,omitempty"`
Level string `json:"level,omitempty"`
Family string `json:"family,omitempty"`
Type string `json:"type,omitempty"`
}
// an operation is an object that maps to an agent module.
// the parameters of the operation are passed to the module as an argument,
// and thus their format depends on the module itself.
type Operation struct {
Module string `json:"module"`
Parameters interface{} `json:"parameters"`
// If WantCompressed is set in the operation, the parameters
// will be compressed in PostAction() when the client sends the
// action to the API. This will also result in IsCompressed being
// marked as true, so the receiving agent knows it must decompress
// the parameter data.
IsCompressed bool `json:"is_compressed,omitempty"`
WantCompressed bool `json:"want_compressed,omitempty"`
}
// Compress the parameters stored within an operation
func (op *Operation) CompressOperationParam() (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("CompressOperationParam() -> %v", e)
}
}()
jb, err := json.Marshal(op.Parameters)
if err != nil {
panic(err)
}
var b bytes.Buffer
wb64 := base64.NewEncoder(base64.StdEncoding, &b)
w := gzip.NewWriter(wb64)
_, err = w.Write(jb)
if err != nil {
panic(err)
}
w.Close()
wb64.Close()
op.Parameters = string(b.Bytes())
op.IsCompressed = true
return
}
// Decompress the parameters stored within an operation
func (op *Operation) DecompressOperationParam() (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("DecompressOperationParam() -> %v", e)
}
}()
if !op.IsCompressed {
return nil
}
pstr, ok := op.Parameters.(string)
if !ok {
panic("Compressed parameter was not a string")
}
b := bytes.NewBuffer([]byte(pstr))
rb64 := base64.NewDecoder(base64.StdEncoding, b)
r, err := gzip.NewReader(rb64)
if err != nil {
panic(err)
}
rb, err := ioutil.ReadAll(r)
if err != nil {
panic(err)
}
err = json.Unmarshal(rb, &op.Parameters)
if err != nil {
panic(err)
}
op.IsCompressed = false
return
}
// ActionFromFile() reads an action from a local file on the file system
// and returns a mig.Action structure
func ActionFromFile(path string) (Action, error) {
var err error
var a Action
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("mig.ActionFromFile(): %v", e)
}
}()
// parse the json of the action into a mig.Action
fd, err := ioutil.ReadFile(path)
if err != nil {
return a, err
}
err = json.Unmarshal(fd, &a)
if err != nil {
return a, err
}
return a, err
}
// ToTempFile writes an action into a generated temporary file and returns its filename
func (a Action) ToTempFile() (filename string, err error) {
var (
data []byte
fd *os.File
fi os.FileInfo
)
data, err = json.Marshal(a)
if err != nil {
return
}
fd, err = ioutil.TempFile("", "migaction_")
defer fd.Close()
if err != nil {
return
}
_, err = fd.Write(data)
if err != nil {
return
}
fi, err = fd.Stat()
if err != nil {
return
}
filename = fmt.Sprintf("%s/%s", os.TempDir(), fi.Name())
return
}
type id struct {
value float64
sync.Mutex
}
var globalID id
// GenID() returns a float64 ID number that is unique to this process. The ID is initialized
// at the number of seconds since MIG's creation date, shifted 16 bits to the right and incremented
// by one every time a new ID is requested. The resulting value must fit in 53 bits of precision
// provided by the float64 type.
func GenID() float64 {
globalID.Lock()
defer globalID.Unlock()
if globalID.value < 1 {
// if id hasn't been initialized yet, set it to number of seconds since
// MIG's inception, plus one
tmpid := int64(time.Since(time.Unix(1367258400, 0)).Seconds() + 1)
tmpid = tmpid << 16
globalID.value = float64(tmpid)
return globalID.value
} else {
globalID.value++
return globalID.value
}
}
// GenHexID returns a string with an hexadecimal encoded ID
func GenB32ID() string {
id := GenID()
return strconv.FormatUint(uint64(id), 36)
}
// Validate verifies that the Action received contained all the
// necessary fields, and returns an error when it doesn't.
func (a Action) Validate() (err error) {
if a.Name == "" {
return errors.New("Action.Name is empty. Expecting string.")
}
if a.Target == "" {
return errors.New("Action.Target is empty. Expecting string.")
}
if a.SyntaxVersion != ActionVersion {
return fmt.Errorf("Wrong Syntax Version integer. Expection version %d", ActionVersion)
}
if a.ValidFrom.String() == "" {
return errors.New("Action.ValidFrom is empty. Expecting string.")
}
if a.ExpireAfter.String() == "" {
return errors.New("Action.ExpireAfter is empty. Expecting string.")
}
if a.ValidFrom.After(a.ExpireAfter) {
return errors.New("Action.ExpireAfter is set before Action.ValidFrom.")
}
if time.Now().After(a.ExpireAfter) {
return errors.New("Action.ExpireAfter is passed. Action has expired.")
}
if a.Operations == nil {
return errors.New("Action.Operations is nil. Expecting string.")
}
if len(a.PGPSignatures) < 1 {
return errors.New("Action.PGPSignatures is empty. Expecting array of strings.")
}
return
}
// Sign computes and returns the GPG signature of a MIG action in its stringified form
func (a Action) Sign(keyid string, secring io.Reader) (sig string, err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("Sign() -> %v", e)
}
}()
filename, err := a.ToTempFile()
if err != nil {
panic(err)
}
a2, err := ActionFromFile(filename)
if err != nil {
panic(err)
}
err = os.Remove(filename)
if err != nil {
panic(err)
}
str, err := a2.String()
if err != nil {
panic(err)
}
sig, err = pgp.Sign(str, keyid, secring)
if err != nil {
panic(err)
}
return
}
// VerifySignatures verifies that the Action contains valid signatures from
// known investigators. It does not verify permissions.
func (a Action) VerifySignatures(keyring io.Reader) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf("VerifySignatures() -> %v", e)
}
}()
astr, err := a.String()
if err != nil {
return errors.New("Failed to stringify action")
}
// Create a copy of the keyring we can use during validation of each
// signature. We don't want to use the keyring reader directly as it is
// backed by a buffer and will be drained after verification of the first
// signature.
keycopy, err := ioutil.ReadAll(keyring)
if err != nil {
panic(err)
}
for _, sig := range a.PGPSignatures {
keyrdr := bytes.NewBuffer(keycopy)
valid, _, err := pgp.Verify(astr, sig, keyrdr)
if err != nil {
return errors.New("Failed to verify PGP Signature")
}
if !valid {
return errors.New("Invalid PGP Signature")
}
}
return
}
// concatenates Action components into a string
func (a Action) String() (str string, err error) {
args, err := json.Marshal(a.Operations)
if err != nil {
return
}
str += fmt.Sprintf("name=%s;target=%s;validfrom=%d;expireafter=%s;operations=%s;",
a.Name, a.Target, a.ValidFrom.UTC().Unix(), a.ExpireAfter.UTC().Unix(), args)
return
}
// VerifyACL controls that an action has been issued by investigators
// that have the right permissions. This function looks at each operation
// listed in the action, and find the corresponding permission. If no
// permission is found, the default one `default` is used.
// The first permission that is found to apply to an operation, but
// doesn't allow the operation to run, will fail the verification globally
func (a Action) VerifyACL(acl ACL, keyring io.Reader, onlyVerifyPubKey bool) (err error) {
// first, verify all signatures and get a list of PGP
// fingerprints of the signers
var fingerprints []string
astr, err := a.String()
if err != nil {
return errors.New("Failed to stringify action")
}
// Create a copy of the keyring we can use during validation of each
// signature. We don't want to use the keyring reader directly as it is
// backed by a buffer and will be drained after verification of the first
// signature.
keycopy, err := ioutil.ReadAll(keyring)
if err != nil {
panic(err)
}
for _, sig := range a.PGPSignatures {
keyrdr := bytes.NewBuffer(keycopy)
fp, err := pgp.GetFingerprintFromSignature(astr, sig, keyrdr)
if err != nil {
return fmt.Errorf("Failed to retrieve fingerprint from signatures: %v", err)
}
fingerprints = append(fingerprints, fp)
}
if len(fingerprints) == 0 {
return errors.New("No valid fingerprints found.")
}
if onlyVerifyPubKey {
return
}
// Then, for each operation contained in the action, look for
// a permission that apply to it, by comparing the operation name
// with permission name. If none is found, use the default permission.
for _, operation := range a.Operations {
for _, permission := range acl {
for permName, _ := range permission {
if permName == operation.Module {
return verifyPermission(operation, permName, permission, fingerprints)
}
}
}
// no specific permission found, apply the default permission
var defaultPerm Permission
for _, permission := range acl {
for permName, _ := range permission {
if permName == "default" {
defaultPerm = permission
break
}
}
}
return verifyPermission(operation, "default", defaultPerm, fingerprints)
}
return
}
// PrintCounters prints the counters of an action to stderr
func (a Action) PrintCounters() {
out := fmt.Sprintf("%d sent, %d done", a.Counters.Sent, a.Counters.Done)
if a.Counters.InFlight > 0 {
out += fmt.Sprintf(", %d inflight", a.Counters.InFlight)
}
if a.Counters.Success > 0 {
out += fmt.Sprintf(", %d succeeded", a.Counters.Success)
}
if a.Counters.Cancelled > 0 {
out += fmt.Sprintf(", %d cancelled", a.Counters.Cancelled)
}
if a.Counters.Expired > 0 {
out += fmt.Sprintf(", %d expired", a.Counters.Expired)
}
if a.Counters.Failed > 0 {
out += fmt.Sprintf(", %d failed", a.Counters.Failed)
}
if a.Counters.TimeOut > 0 {
out += fmt.Sprintf(", %d timed out", a.Counters.TimeOut)
}
fmt.Fprintf(os.Stderr, "%s\n", out)
}
// Return the an indented JSON string representing the action suitable for
// display
func (a Action) IndentedString() (string, error) {
buf, err := json.MarshalIndent(a, "", " ")
if err != nil {
return "", err
}
return string(buf), nil
}