package checks import ( "context" "encoding/json" "os/exec" "strings" "time" ) // AuthStatus holds the result of an AWS auth check via the awsmfa tool. type AuthStatus struct { Authed bool Account string Expires time.Time // zero if unknown; session expiry from awsmfa Detail string // human summary line Err string // populated when awsmfa itself failed to run } // notAuthedMarker is the exact string awsmfa prints when the session is gone. const notAuthedMarker = "Not authenticated." // AWSAuth runs ` status` and decides auth state from its output. // This mirrors the shell `myd auth` check — awsmfa is the real MFA session, // not `aws sts`. awsmfaPath is the full path to the awsmfa command. // // Authed output looks like: // // Success. // { "UserId": ..., "Account": "0482...", "Arn": ... } // Expire: 2026-06-12T19:25:19+00:00 func AWSAuth(ctx context.Context, awsmfaPath string) AuthStatus { ctx, cancel := context.WithTimeout(ctx, 8*time.Second) defer cancel() out, err := exec.CommandContext(ctx, awsmfaPath, "status").CombinedOutput() text := strings.TrimSpace(string(out)) if err != nil && text == "" { return AuthStatus{Authed: false, Err: err.Error()} } if strings.Contains(text, notAuthedMarker) { return AuthStatus{Authed: false, Detail: notAuthedMarker} } s := AuthStatus{Authed: true} // Account: decode the JSON identity block. Use a Decoder so the trailing // "Expire:" line after the closing brace doesn't fail parsing. if i := strings.IndexByte(text, '{'); i >= 0 { var id struct { Account string `json:"Account"` } if json.NewDecoder(strings.NewReader(text[i:])).Decode(&id) == nil { s.Account = id.Account } } // Expiry: the "Expire: " line. for _, line := range strings.Split(text, "\n") { if rest, ok := strings.CutPrefix(strings.TrimSpace(line), "Expire:"); ok { if t, perr := time.Parse(time.RFC3339, strings.TrimSpace(rest)); perr == nil { s.Expires = t } } } s.Detail = summarize(s.Account, s.Expires) return s } func summarize(account string, expires time.Time) string { var parts []string if account != "" { parts = append(parts, "account "+account) } if !expires.IsZero() { parts = append(parts, "expires "+expires.Local().Format("15:04")) } if len(parts) == 0 { return "authed" } return strings.Join(parts, " · ") }