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 }