package internal import ( "encoding/csv" "encoding/json" "fmt" "log" "os" "path" "path/filepath" "github.com/aquasecurity/table" "github.com/aws/smithy-go/ptr" "github.com/spf13/afero" "golang.org/x/crypto/ssh/terminal" ) // This struct is here to maintain compatibility with legacy cloudfox code type OutputData2 struct { Headers []string Body [][]string FilePath string FullFilename string CallingModule string Verbosity int Directory string } // verbosity = 1 (Output and loot printed to file). // verbosity = 2 (Output and loot printed to file, output printed screen). // verbosity = 3 (Output and loot printed to file and screen). // outputType = "table", "csv" // prefixIdentifier = this string gets printed with control message calling module (e.g. aws profile, azure resource group, gcp project, etc) func OutputSelector(verbosity int, outputType string, header []string, body [][]string, outputDirectory string, fileName string, callingModule string, wrapTable bool, prefixIdentifier string) { switch verbosity { case 2: PrintTableToScreen(header, body, wrapTable) case 3: PrintTableToScreen(header, body, wrapTable) // Add writeLootToScreen function here } switch outputType { case "wide": case "table": fmt.Println("") outputFileTable := createOutputFile( ptr.String(filepath.Join(outputDirectory, "table")), ptr.String(fmt.Sprintf("%s.txt", fileName)), outputType, callingModule) printTableToFile(header, body, wrapTable, outputFileTable) fmt.Printf("[%s][%s] Output written to [%s]\n", cyan(callingModule), cyan(prefixIdentifier), outputFileTable.Name()) // Add writeLootToFile function here case "csv": outputFileCSV := createOutputFile( ptr.String(filepath.Join(outputDirectory, "csv")), ptr.String(fmt.Sprintf("%s.csv", fileName)), outputType, callingModule) printCSVtoFile(header, body, outputFileCSV) fmt.Printf("[%s][%s] Output written to [%s]\n", cyan(callingModule), cyan(prefixIdentifier), outputFileCSV.Name()) // Add writeLootToFile function here default: outputFileTable := createOutputFile( ptr.String(filepath.Join(outputDirectory, "table")), ptr.String(fmt.Sprintf("%s.txt", fileName)), outputType, callingModule) printTableToFile(header, body, wrapTable, outputFileTable) fmt.Printf("[%s][%s] Output written to [%s]\n", cyan(callingModule), cyan(prefixIdentifier), outputFileTable.Name()) outputFileCSV := createOutputFile( ptr.String(filepath.Join(outputDirectory, "csv")), ptr.String(fmt.Sprintf("%s.csv", fileName)), outputType, callingModule) printCSVtoFile(header, body, outputFileCSV) fmt.Printf("[%s][%s] Output written to [%s]\n", cyan(callingModule), cyan(prefixIdentifier), outputFileCSV.Name()) // Add writeLootToFile function here outputFileJSON := createOutputFile( ptr.String(filepath.Join(outputDirectory, "json")), ptr.String(fmt.Sprintf("%s.json", fileName)), outputType, callingModule) printJsonToFile(header, body, outputFileJSON) fmt.Printf("[%s][%s] Output written to [%s]\n", cyan(callingModule), cyan(prefixIdentifier), outputFileJSON.Name()) } } func printCSVtoFile(header []string, body [][]string, outputFile afero.File) { csvWriter := csv.NewWriter(outputFile) csvWriter.Write(header) for _, row := range body { row = removeColorCodesFromSlice(row) csvWriter.Write(row) } csvWriter.Flush() } func printJsonToFile(header []string, body [][]string, outputFile afero.File) { body = removeColorCodesFromNestedSlice(body) jsonData := make([]map[string]string, len(body)) for i, row := range body { jsonData[i] = make(map[string]string) for j, column := range row { jsonData[i][header[j]] = column } } jsonBytes, err := json.MarshalIndent(jsonData, "", " ") if err != nil { fmt.Println("error marshalling json:", err) return } outputFile.Write(jsonBytes) } func printTableToFile(header []string, body [][]string, wrapLines bool, outputFile afero.File) { body = removeColorCodesFromNestedSlice(body) standardColumnWidth := 1000 t := table.New(outputFile) if wrapLines { terminalWidth, _, err := terminal.GetSize(int(os.Stdout.Fd())) if err != nil { fmt.Println("error getting terminal size:", err) return } columnCount := len(header) // The offset value was defined by trial and error to get the best wrapping trialAndErrorOffset := 1 standardColumnWidth = terminalWidth / (columnCount + trialAndErrorOffset) } t.SetColumnMaxWidth(standardColumnWidth) t.SetHeaders(header...) t.AddRows(body...) t.SetRowLines(false) t.SetDividers(table.UnicodeRoundedDividers) t.SetAlignment(table.AlignLeft) t.Render() } func PrintTableToScreen(header []string, body [][]string, wrapLines bool) { standardColumnWidth := 1000 t := table.New(os.Stdout) if wrapLines { terminalWidth, _, err := terminal.GetSize(int(os.Stdout.Fd())) if err != nil { fmt.Println("error getting terminal size:", err) return } columnCount := len(header) // The offset value was defined by trial and error to get the best wrapping trialAndErrorOffset := 1 standardColumnWidth = terminalWidth / (columnCount + trialAndErrorOffset) } t.SetColumnMaxWidth(standardColumnWidth) t.SetHeaders(header...) t.AddRows(body...) t.SetHeaderStyle(table.StyleBold) t.SetRowLines(false) t.SetLineStyle(table.StyleCyan) t.SetDividers(table.UnicodeRoundedDividers) t.SetAlignment(table.AlignLeft) t.Render() } // The Afero library enables file system mocking: // fileSystem = afero.NewOsFs() if not unit testing (real file system) OR // fileSystem = afero.NewMemMapFs() for a mocked file system (when unit testing) // outputDirectory = nil (creates the file in the current directory ".") func createOutputFile(outputDirectory *string, fileName *string, outputType string, callingModule string) afero.File { if outputDirectory == nil { outputDirectory = ptr.String(".") } if _, err := fileSystem.Stat(ptr.ToString(outputDirectory)); os.IsNotExist(err) { err = fileSystem.MkdirAll(ptr.ToString(outputDirectory), 0700) if err != nil { log.Fatal(err) } } if fileName == nil { log.Fatalf("Error creating output file because no file name was specified") } outputFile, err := fileSystem.OpenFile(path.Join(ptr.ToString(outputDirectory), ptr.ToString(fileName)), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { log.Fatalf("error creating output file: %s", err) } //fmt.Printf("[%s] Creating output file: %s\n", cyan(callingModule), outputFile.Name()) return outputFile } func MockFileSystem(switcher bool) afero.Fs { if switcher { fmt.Println("Using mocked file system") fileSystem = afero.NewMemMapFs() return fileSystem } else { fmt.Println("Using OS file system. Make sure to clean up your disk!") fileSystem = afero.NewOsFs() return fileSystem } }