github.com/keybase/client/go@v0.0.0-20240520164431-4f512a4c85a3/client/cmd_audit.go (about)

     1  // Copyright 2019 Keybase, Inc. All rights reserved. Use of
     2  // this source code is governed by the included BSD license.
     3  
     4  package client
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/keybase/cli"
    11  	"github.com/keybase/client/go/libcmdline"
    12  	"github.com/keybase/client/go/libkb"
    13  	"github.com/keybase/client/go/protocol/keybase1"
    14  	"golang.org/x/net/context"
    15  )
    16  
    17  func NewCmdAudit(cl *libcmdline.CommandLine, g *libkb.GlobalContext) cli.Command {
    18  	commands := []cli.Command{
    19  		NewCmdAuditBox(cl, g),
    20  	}
    21  
    22  	return cli.Command{
    23  		Name: "audit",
    24  		// No 'Usage' makes this hidden
    25  		Description: "Perform security audits",
    26  		Subcommands: commands,
    27  	}
    28  }
    29  
    30  type CmdAuditBox struct {
    31  	libkb.Contextified
    32  	AuditAllKnownTeams  bool
    33  	IsInJail            bool
    34  	Audit               bool
    35  	Attempt             bool
    36  	RotateBeforeAttempt bool
    37  	Ls                  bool
    38  	TeamID              keybase1.TeamID
    39  	TeamName            string
    40  }
    41  
    42  func NewCmdAuditBox(cl *libcmdline.CommandLine, g *libkb.GlobalContext) cli.Command {
    43  	cmd := &CmdAuditBox{
    44  		Contextified: libkb.NewContextified(g),
    45  	}
    46  	return cli.Command{
    47  		Name: "box",
    48  		Usage: `A team box audit makes sure a team's secrets are encrypted for
    49  	the right members in the team, and that when members revoke devices or
    50  	reset their accounts, the team's secret keys are rotated accordingly.`,
    51  		Flags: []cli.Flag{
    52  			cli.BoolFlag{
    53  				Name: "audit-all-known-teams",
    54  				Usage: `Audit all known teams. If an audit fails, the team will
    55  	be rotated and the audit will be retried immediately . If it fails again,
    56  	this error will be reported. This operation may take a long time if you are
    57  	in many teams.`,
    58  			},
    59  			cli.StringFlag{
    60  				Name:  "team-id",
    61  				Usage: "Team ID, required (or team name) except for list-known-team-ids/audit-all-known-teams",
    62  			},
    63  			cli.StringFlag{
    64  				Name:  "team",
    65  				Usage: "Team name, required (or team ID) except for list-known-team-ids/audit-all-known-teams",
    66  			},
    67  			cli.BoolFlag{
    68  				Name:  "is-in-jail",
    69  				Usage: "Check if a team id is in the box audit jail",
    70  			},
    71  			cli.BoolFlag{
    72  				Name:  "audit",
    73  				Usage: "Audit a team id, storing result to disk and scheduling additional background reaudits if it failed",
    74  			},
    75  			cli.BoolFlag{
    76  				Name:  "attempt",
    77  				Usage: "Audit a team id without persisting results anywhere",
    78  			},
    79  			cli.BoolFlag{
    80  				Name:  "rotate-before-attempt",
    81  				Usage: "Only valid with --attempt; rotate the team's keys first when given.",
    82  			},
    83  			cli.BoolFlag{
    84  				Name:  "list-known-team-ids",
    85  				Usage: "List all known team ids",
    86  			},
    87  		},
    88  		ArgumentHelp: "",
    89  		Action: func(c *cli.Context) {
    90  			cl.ChooseCommand(cmd, "box", c)
    91  		},
    92  	}
    93  }
    94  
    95  func b2i(x bool) int {
    96  	if x {
    97  		return 1
    98  	}
    99  	return 0
   100  }
   101  
   102  func (c *CmdAuditBox) ParseArgv(ctx *cli.Context) error {
   103  	c.AuditAllKnownTeams = ctx.Bool("audit-all-known-teams")
   104  	c.IsInJail = ctx.Bool("is-in-jail")
   105  	c.Audit = ctx.Bool("audit")
   106  	c.Attempt = ctx.Bool("attempt")
   107  	c.Ls = ctx.Bool("list-known-team-ids")
   108  	if b2i(c.AuditAllKnownTeams)+b2i(c.IsInJail)+b2i(c.Audit)+b2i(c.Attempt)+b2i(c.Ls) != 1 {
   109  		return fmt.Errorf("need a single command: audit-all-known-teams, is-in-jail, audit, attempt, or list-known-team-ids")
   110  	}
   111  	c.RotateBeforeAttempt = ctx.Bool("rotate-before-attempt")
   112  	if c.RotateBeforeAttempt && !c.Attempt {
   113  		return fmt.Errorf("can only use --rotate-before-attempt with --attempt")
   114  	}
   115  
   116  	c.TeamID = keybase1.TeamID(ctx.String("team-id"))
   117  	c.TeamName = ctx.String("team")
   118  	if len(c.TeamID) != 0 && len(c.TeamName) != 0 {
   119  		return fmt.Errorf("cannot provide both team id and team name")
   120  	}
   121  	gaveTeam := c.TeamID != "" || c.TeamName != ""
   122  
   123  	if c.Ls || c.AuditAllKnownTeams {
   124  		if gaveTeam {
   125  			return fmt.Errorf("cannot provide team with this option")
   126  		}
   127  	} else {
   128  		if !gaveTeam {
   129  			return fmt.Errorf("need team id or team name")
   130  		}
   131  	}
   132  
   133  	return nil
   134  }
   135  
   136  func (c *CmdAuditBox) Run() error {
   137  	cli, err := GetAuditClient(c.G())
   138  	if err != nil {
   139  		return err
   140  	}
   141  
   142  	ctx := context.Background()
   143  
   144  	if c.TeamName != "" {
   145  		cli, err := GetTeamsClient(c.G())
   146  		if err != nil {
   147  			return err
   148  		}
   149  		teamID, err := cli.GetTeamID(ctx, c.TeamName)
   150  		if err != nil {
   151  			return err
   152  		}
   153  		c.TeamID = teamID
   154  	}
   155  
   156  	var failedTeamIDs []keybase1.TeamID
   157  	switch {
   158  	case c.AuditAllKnownTeams:
   159  		knownTeamIDs, err := cli.KnownTeamIDs(ctx, 0)
   160  		if err != nil {
   161  			return err
   162  		}
   163  
   164  		// team name, 1/...
   165  		for idx, teamID := range knownTeamIDs {
   166  			arg := keybase1.BoxAuditTeamArg{TeamID: teamID}
   167  
   168  			var err error
   169  			var attempt *keybase1.BoxAuditAttempt
   170  			attempt, err = cli.BoxAuditTeam(ctx, arg)
   171  			prefix := fmt.Sprintf("(%d/%d) %s", idx+1, len(knownTeamIDs), teamID)
   172  			describeAttempt(c.G(), attempt, prefix)
   173  			if err != nil {
   174  				attempt, err = cli.BoxAuditTeam(ctx, arg)
   175  				describeAttempt(c.G(), attempt, "(retry) "+prefix)
   176  			}
   177  			if err != nil {
   178  				c.G().Log.Error("Audit failed for %s: %s", teamID, err)
   179  				failedTeamIDs = append(failedTeamIDs, teamID)
   180  			}
   181  		}
   182  
   183  		if failedTeamIDs != nil {
   184  			var teamIDStrs []string
   185  			for _, teamID := range failedTeamIDs {
   186  				teamIDStrs = append(teamIDStrs, teamID.String())
   187  			}
   188  			return fmt.Errorf("The following teams failed to pass an audit. This does not necessarily mean something is wrong, unless you are a member of those teams.\n%s", strings.Join(teamIDStrs, ", "))
   189  		}
   190  		return nil
   191  	case c.IsInJail:
   192  		ok, err := cli.IsInJail(ctx, keybase1.IsInJailArg{TeamID: c.TeamID})
   193  		if err != nil {
   194  			return err
   195  		}
   196  		fmt.Println(ok)
   197  		return nil
   198  	case c.Audit:
   199  		attempt, err := cli.BoxAuditTeam(ctx, keybase1.BoxAuditTeamArg{TeamID: c.TeamID})
   200  		describeAttempt(c.G(), attempt, "")
   201  		return err
   202  	case c.Attempt:
   203  		arg := keybase1.AttemptBoxAuditArg{TeamID: c.TeamID, RotateBeforeAudit: c.RotateBeforeAttempt}
   204  		audit, err := cli.AttemptBoxAudit(ctx, arg)
   205  		if err != nil {
   206  			return err
   207  		}
   208  		fmt.Printf("%s\n", audit.Ctime.Time())
   209  		fmt.Printf("Result: %s\n", audit.Result)
   210  		if audit.Generation != nil {
   211  			fmt.Printf("Team generation: %d\n", *audit.Generation)
   212  		}
   213  		if audit.Error != nil {
   214  			c.G().Log.Error("Box audit attempt failed: %s\n", *audit.Error)
   215  		}
   216  		return nil
   217  	case c.Ls:
   218  		ids, err := cli.KnownTeamIDs(ctx, 0)
   219  		if err != nil {
   220  			return err
   221  		}
   222  		for _, id := range ids {
   223  			fmt.Println(id)
   224  		}
   225  		return nil
   226  	default:
   227  		return fmt.Errorf("no command given")
   228  	}
   229  }
   230  
   231  func (c *CmdAuditBox) GetUsage() libkb.Usage {
   232  	return libkb.Usage{
   233  		Config: true,
   234  		API:    true,
   235  	}
   236  }
   237  
   238  func describeAttempt(g *libkb.GlobalContext, attempt *keybase1.BoxAuditAttempt, info string) {
   239  	tui := g.UI.GetTerminalUI()
   240  	prefix := ""
   241  	if info != "" {
   242  		prefix = info + " "
   243  	}
   244  	if attempt == nil {
   245  		_, _ = tui.PrintfUnescaped("%s\n", ColorString(g, "red", prefix+"Audit not attempted."))
   246  	} else if attempt.Error == nil {
   247  		_, _ = tui.PrintfUnescaped("%s\n", ColorString(g, "green", prefix+attempt.String()))
   248  	} else {
   249  		_, _ = tui.PrintfUnescaped("%s\n", ColorString(g, "red", prefix+attempt.String()))
   250  	}
   251  }