github.com/minio/mc@v0.0.0-20240503112107-b471de8d1882/cmd/support-inspect.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package cmd
    19  
    20  import (
    21  	"context"
    22  	"encoding/base64"
    23  	"encoding/binary"
    24  	"encoding/hex"
    25  	"fmt"
    26  	"hash/crc32"
    27  	"io"
    28  	"os"
    29  	"path/filepath"
    30  	"runtime"
    31  	"strings"
    32  	"time"
    33  
    34  	"github.com/fatih/color"
    35  	"github.com/minio/cli"
    36  	json "github.com/minio/colorjson"
    37  	"github.com/minio/madmin-go/v3"
    38  	"github.com/minio/mc/pkg/probe"
    39  	"github.com/minio/pkg/v2/console"
    40  )
    41  
    42  const (
    43  	defaultPublicKey      = "MIIBCgKCAQEAs/128UFS9A8YSJY1XqYKt06dLVQQCGDee69T+0Tip/1jGAB4z0/3QMpH0MiS8Wjs4BRWV51qvkfAHzwwdU7y6jxU05ctb/H/WzRj3FYdhhHKdzear9TLJftlTs+xwj2XaADjbLXCV1jGLS889A7f7z5DgABlVZMQd9BjVAR8ED3xRJ2/ZCNuQVJ+A8r7TYPGMY3wWvhhPgPk3Lx4WDZxDiDNlFs4GQSaESSsiVTb9vyGe/94CsCTM6Cw9QG6ifHKCa/rFszPYdKCabAfHcS3eTr0GM+TThSsxO7KfuscbmLJkfQev1srfL2Ii2RbnysqIJVWKEwdW05ID8ryPkuTuwIDAQAB"
    44  	inspectOutputFilename = "inspect-data.enc"
    45  )
    46  
    47  var supportInspectFlags = append(subnetCommonFlags,
    48  	cli.BoolFlag{
    49  		Name:  "legacy",
    50  		Usage: "use the older inspect format",
    51  	},
    52  )
    53  
    54  var supportInspectCmd = cli.Command{
    55  	Name:            "inspect",
    56  	Usage:           "upload raw object contents for analysis",
    57  	Action:          mainSupportInspect,
    58  	OnUsageError:    onUsageError,
    59  	Before:          setGlobalsFromContext,
    60  	Flags:           supportInspectFlags,
    61  	HideHelpCommand: true,
    62  	CustomHelpTemplate: `NAME:
    63    {{.HelpName}} - {{.Usage}}
    64  
    65  USAGE:
    66    {{.HelpName}} [FLAGS] TARGET
    67  
    68  FLAGS:
    69    {{range .VisibleFlags}}{{.}}
    70    {{end}}
    71  EXAMPLES:
    72    1. Upload 'xl.meta' of a specific object from all the drives
    73       {{.Prompt}} {{.HelpName}} myminio/bucket/test*/xl.meta
    74  
    75    2. Upload recursively all objects at a prefix. NOTE: This can be an expensive operation use it with caution.
    76       {{.Prompt}} {{.HelpName}} myminio/bucket/test/**
    77  
    78    3. Download 'xl.meta' of a specific object from all the drives locally, and upload to SUBNET manually
    79       {{.Prompt}} {{.HelpName}} myminio/bucket/test*/xl.meta --airgap
    80  `,
    81  }
    82  
    83  type inspectMessage struct {
    84  	Status     string `json:"status"`
    85  	AliasedURL string `json:"aliasedURL,omitempty"`
    86  	File       string `json:"file,omitempty"`
    87  	Key        string `json:"key,omitempty"`
    88  }
    89  
    90  // Colorized message for console printing.
    91  func (t inspectMessage) String() string {
    92  	var msg string
    93  	if globalAirgapped {
    94  		if t.Key == "" {
    95  			msg = fmt.Sprintf("File data successfully downloaded as %s", console.Colorize("File", t.File))
    96  		} else {
    97  			msg = fmt.Sprintf("Encrypted file data successfully downloaded as %s\n", console.Colorize("File", t.File))
    98  			msg += fmt.Sprintf("Decryption key: %s\n\n", console.Colorize("Key", t.Key))
    99  
   100  			msg += "The decryption key will ONLY be shown here. It cannot be recovered.\n"
   101  			msg += "The encrypted file can safely be shared without the decryption key.\n"
   102  			msg += "Even with the decryption key, data stored with encryption cannot be accessed."
   103  		}
   104  	} else {
   105  		msg = fmt.Sprintf("Object inspection data for '%s' uploaded to SUBNET successfully", t.AliasedURL)
   106  	}
   107  	return console.Colorize(supportSuccessMsgTag, msg)
   108  }
   109  
   110  func (t inspectMessage) JSON() string {
   111  	t.Status = "success"
   112  	jsonMessageBytes, e := json.MarshalIndent(t, "", " ")
   113  	fatalIf(probe.NewError(e), "Unable to marshal into JSON.")
   114  	return string(jsonMessageBytes)
   115  }
   116  
   117  func checkSupportInspectSyntax(ctx *cli.Context) {
   118  	if len(ctx.Args()) != 1 {
   119  		showCommandHelpAndExit(ctx, 1) // last argument is exit code
   120  	}
   121  }
   122  
   123  // mainSupportInspect - the entry function of inspect command
   124  func mainSupportInspect(ctx *cli.Context) error {
   125  	// Check for command syntax
   126  	checkSupportInspectSyntax(ctx)
   127  
   128  	setSuccessMessageColor()
   129  
   130  	// Get the alias parameter from cli
   131  	args := ctx.Args()
   132  	aliasedURL := args.Get(0)
   133  
   134  	alias, apiKey := initSubnetConnectivity(ctx, aliasedURL, true)
   135  	if len(apiKey) == 0 {
   136  		// api key not passed as flag. Check that the cluster is registered.
   137  		apiKey = validateClusterRegistered(alias, true)
   138  	}
   139  
   140  	console.SetColor("File", color.New(color.FgWhite, color.Bold))
   141  	console.SetColor("Key", color.New(color.FgHiRed, color.Bold))
   142  
   143  	// Create a new MinIO Admin Client
   144  	client, err := newAdminClient(aliasedURL)
   145  	if err != nil {
   146  		fatalIf(err.Trace(aliasedURL), "Unable to initialize admin client.")
   147  		return nil
   148  	}
   149  
   150  	// Compute bucket and object from the aliased URL
   151  	aliasedURL = filepath.ToSlash(aliasedURL)
   152  	splits := splitStr(aliasedURL, "/", 3)
   153  	bucket, prefix := splits[1], splits[2]
   154  
   155  	shellName, _ := getShellName()
   156  	if runtime.GOOS != "windows" && shellName != "bash" && strings.Contains(prefix, "*") {
   157  		console.Infoln("Your shell is auto determined as '" + shellName + "', wildcard patterns are only supported with 'bash' SHELL.")
   158  	}
   159  
   160  	var publicKey []byte
   161  	if !ctx.Bool("legacy") {
   162  		var e error
   163  		publicKey, e = os.ReadFile(filepath.Join(mustGetMcConfigDir(), "support_public.pem"))
   164  		if e != nil && !os.IsNotExist(e) {
   165  			fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to inspect file.")
   166  		} else if len(publicKey) > 0 {
   167  			if !globalJSON && !globalQuiet {
   168  				console.Infoln("Using public key from ", filepath.Join(mustGetMcConfigDir(), "support_public.pem"))
   169  			}
   170  		}
   171  
   172  		// Fall back to MinIO public key.
   173  		if len(publicKey) == 0 {
   174  			// Public key for MinIO confidential information.
   175  			publicKey, _ = base64.StdEncoding.DecodeString(defaultPublicKey)
   176  		}
   177  	}
   178  
   179  	key, r, e := client.Inspect(context.Background(), madmin.InspectOptions{
   180  		Volume:    bucket,
   181  		File:      prefix,
   182  		PublicKey: publicKey,
   183  	})
   184  	fatalIf(probe.NewError(e).Trace(aliasedURL), "Unable to inspect file.")
   185  
   186  	// Download the inspect data in a temporary file first
   187  	tmpFile, e := os.CreateTemp("", "mc-inspect-")
   188  	fatalIf(probe.NewError(e), "Unable to download file data.")
   189  	_, e = io.Copy(tmpFile, r)
   190  	fatalIf(probe.NewError(e), "Unable to download file data.")
   191  	r.Close()
   192  	tmpFile.Close()
   193  
   194  	if globalAirgapped {
   195  		saveInspectDataFile(key, tmpFile)
   196  		return nil
   197  	}
   198  
   199  	uploadURL := SubnetUploadURL("inspect")
   200  	reqURL, headers := prepareSubnetUploadURL(uploadURL, alias, apiKey)
   201  
   202  	tmpFileName := tmpFile.Name()
   203  	_, e = (&SubnetFileUploader{
   204  		alias:             alias,
   205  		FilePath:          tmpFileName,
   206  		filename:          inspectOutputFilename,
   207  		ReqURL:            reqURL,
   208  		Headers:           headers,
   209  		DeleteAfterUpload: true,
   210  	}).UploadFileToSubnet()
   211  	if e != nil {
   212  		console.Errorln("Unable to upload inspect data to SUBNET portal: " + e.Error())
   213  		saveInspectDataFile(key, tmpFile)
   214  		return nil
   215  	}
   216  
   217  	printMsg(inspectMessage{AliasedURL: aliasedURL})
   218  	return nil
   219  }
   220  
   221  func saveInspectDataFile(key []byte, tmpFile *os.File) {
   222  	var keyHex string
   223  
   224  	downloadPath := inspectOutputFilename
   225  	// Choose a name and move the inspect data to its final destination
   226  	if key != nil {
   227  		// Create an id that is also crc.
   228  		var id [4]byte
   229  		binary.LittleEndian.PutUint32(id[:], crc32.ChecksumIEEE(key[:]))
   230  		// We use 4 bytes of the 32 bytes to identify they file.
   231  		downloadPath = fmt.Sprintf("inspect-data.%s.enc", hex.EncodeToString(id[:]))
   232  		keyHex = hex.EncodeToString(id[:]) + hex.EncodeToString(key[:])
   233  	}
   234  
   235  	fi, e := os.Stat(downloadPath)
   236  	if e == nil && !fi.IsDir() {
   237  		e = moveFile(downloadPath, downloadPath+"."+time.Now().Format(dateTimeFormatFilename))
   238  		fatalIf(probe.NewError(e), "Unable to create a backup of "+downloadPath)
   239  	} else {
   240  		if !os.IsNotExist(e) {
   241  			fatal(probe.NewError(e), "Unable to download file data")
   242  		}
   243  	}
   244  
   245  	fatalIf(probe.NewError(moveFile(tmpFile.Name(), downloadPath)), "Unable to rename downloaded data, file exists at %s", tmpFile.Name())
   246  
   247  	printMsg(inspectMessage{
   248  		File: downloadPath,
   249  		Key:  keyHex,
   250  	})
   251  }