package app import ( "fmt" "io" "log" "net/http" "strings" "time" "github.com/xuri/excelize/v2" ) const maxAuditExportRows = 1_048_572 func (s *Server) handleAuditExport(w http.ResponseWriter, r *http.Request) { tech, err := s.currentTechnician(r) if err != nil { http.Redirect(w, r, "/", http.StatusSeeOther) return } filters := auditFiltersFromRequest(r) records, err := s.listAuditEvents(r, filters, maxAuditExportRows+1) if err != nil { http.Error(w, "unable to export audit events", http.StatusInternalServerError) return } if len(records) > maxAuditExportRows { http.Error( w, "audit export is too large for a single Excel worksheet; narrow the filters and try again", http.StatusRequestEntityTooLarge, ) return } _ = s.audit( r.Context(), "audit_exported", tech.Login, nil, "", "", s.clientIP(r), fmt.Sprintf("format=xlsx rows=%d range=%s", len(records), filters.TimeRange), ) exportedAt := time.Now() filename := "tapm-audit-" + exportedAt.In(s.displayTimeZone()).Format("20060102-150405") + ".xlsx" w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") w.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`) w.Header().Set("Cache-Control", "no-store") if err := writeAuditWorkbook(w, records, filters, exportedAt, s.displayTimeZone()); err != nil { log.Printf("write audit Excel export: %v", err) } } func (s *Server) displayTimeZone() *time.Location { if s.cfg.DisplayTimeZone != nil { return s.cfg.DisplayTimeZone } return time.UTC } func writeAuditWorkbook( output io.Writer, records []auditRecord, filters auditFilters, exportedAt time.Time, displayTimeZone *time.Location, ) error { if displayTimeZone == nil { displayTimeZone = time.UTC } workbook := excelize.NewFile() defer func() { if err := workbook.Close(); err != nil { log.Printf("close audit Excel workbook: %v", err) } }() const sheet = "Audit Log" if err := workbook.SetSheetName("Sheet1", sheet); err != nil { return err } if err := workbook.SetDocProps(&excelize.DocProperties{ Title: "TAPM Audit Log", Creator: "TAPM Broker", LastModifiedBy: "TAPM Broker", Created: exportedAt.UTC().Format(time.RFC3339), Modified: exportedAt.UTC().Format(time.RFC3339), }); err != nil { return err } styles, err := newAuditWorkbookStyles(workbook) if err != nil { return err } stream, err := workbook.NewStreamWriter(sheet) if err != nil { return err } if err := configureAuditWorksheet(stream); err != nil { return err } if err := stream.SetRow( "A1", styledAuditRow(styles.title, "TAPM Audit Log"), excelize.RowOpts{Height: 30}, ); err != nil { return err } if err := stream.MergeCell("A1", "H1"); err != nil { return err } metadata := fmt.Sprintf( "Exported %s · %s · %d event(s)", exportedAt.In(displayTimeZone).Format("Jan 2, 2006 3:04 PM MST"), auditFilterDescription(filters), len(records), ) if err := stream.SetRow( "A2", styledAuditRow(styles.metadata, metadata), excelize.RowOpts{Height: 24}, ); err != nil { return err } if err := stream.MergeCell("A2", "H2"); err != nil { return err } if err := stream.SetRow("A3", []interface{}{nil}, excelize.RowOpts{Height: 8}); err != nil { return err } headers := []string{ "Timestamp", "Event", "Customer / Deployment", "User", "Hostname", "Package ID", "Source IP", "Details", } headerRow := make([]interface{}, len(headers)) for index, header := range headers { headerRow[index] = excelize.Cell{StyleID: styles.header, Value: header} } if err := stream.SetRow("A4", headerRow, excelize.RowOpts{Height: 24}); err != nil { return err } for index, record := range records { rowNumber := index + 5 local := record.CreatedAt.In(displayTimeZone) displayedTime := time.Date( local.Year(), local.Month(), local.Day(), local.Hour(), local.Minute(), local.Second(), local.Nanosecond(), time.UTC, ) row := []interface{}{ excelize.Cell{StyleID: styles.timestamp, Value: displayedTime}, excelize.Cell{StyleID: styles.body, Value: record.EventType}, excelize.Cell{StyleID: styles.body, Value: record.CustomerLabel}, excelize.Cell{StyleID: styles.body, Value: record.User}, excelize.Cell{StyleID: styles.body, Value: record.Hostname}, excelize.Cell{StyleID: styles.body, Value: record.PackageSlug}, excelize.Cell{StyleID: styles.body, Value: record.SourceIP}, excelize.Cell{StyleID: styles.details, Value: record.Details}, } if err := stream.SetRow(fmt.Sprintf("A%d", rowNumber), row); err != nil { return err } } if len(records) > 0 { lastRow := len(records) + 4 showStripes := false if err := stream.AddTable(&excelize.Table{ Range: fmt.Sprintf("A4:H%d", lastRow), Name: "AuditLog", StyleName: "TableStyleMedium2", ShowRowStripes: &showStripes, ShowFirstColumn: false, ShowLastColumn: false, }); err != nil { return err } } if err := stream.Flush(); err != nil { return err } return workbook.Write(output) } type auditWorkbookStyles struct { title int metadata int header int body int timestamp int details int } func newAuditWorkbookStyles(workbook *excelize.File) (auditWorkbookStyles, error) { var styles auditWorkbookStyles var err error styles.title, err = workbook.NewStyle(&excelize.Style{ Font: &excelize.Font{ Bold: true, Family: "Aptos Display", Size: 18, Color: "FFFFFF", }, Fill: excelize.Fill{Type: "pattern", Color: []string{"33404A"}, Pattern: 1}, Alignment: &excelize.Alignment{Vertical: "center"}, }) if err != nil { return styles, err } styles.metadata, err = workbook.NewStyle(&excelize.Style{ Font: &excelize.Font{Family: "Aptos", Size: 11, Color: "33404A"}, Fill: excelize.Fill{Type: "pattern", Color: []string{"F4F7ED"}, Pattern: 1}, Alignment: &excelize.Alignment{ Vertical: "center", }, }) if err != nil { return styles, err } styles.header, err = workbook.NewStyle(&excelize.Style{ Font: &excelize.Font{ Bold: true, Family: "Aptos", Size: 11, Color: "FFFFFF", }, Fill: excelize.Fill{Type: "pattern", Color: []string{"33404A"}, Pattern: 1}, Alignment: &excelize.Alignment{Vertical: "center"}, }) if err != nil { return styles, err } bodyStyle := excelize.Style{ Font: &excelize.Font{Family: "Aptos", Size: 11, Color: "33404A"}, Border: []excelize.Border{{Type: "bottom", Color: "D9DED4", Style: 1}}, Alignment: &excelize.Alignment{Vertical: "top"}, } styles.body, err = workbook.NewStyle(&bodyStyle) if err != nil { return styles, err } numberFormat := "yyyy-mm-dd hh:mm:ss" timestampStyle := bodyStyle timestampStyle.CustomNumFmt = &numberFormat styles.timestamp, err = workbook.NewStyle(×tampStyle) if err != nil { return styles, err } detailsStyle := bodyStyle detailsStyle.Alignment = &excelize.Alignment{Vertical: "top", WrapText: true} styles.details, err = workbook.NewStyle(&detailsStyle) return styles, err } func configureAuditWorksheet(stream *excelize.StreamWriter) error { widths := []float64{22, 28, 30, 22, 24, 24, 18, 54} for index, width := range widths { if err := stream.SetColWidth(index+1, index+1, width); err != nil { return err } } return stream.SetPanes(&excelize.Panes{ Freeze: true, YSplit: 4, TopLeftCell: "A5", ActivePane: "bottomLeft", Selection: []excelize.Selection{{ SQRef: "A5", ActiveCell: "A5", Pane: "bottomLeft", }}, }) } func styledAuditRow(style int, firstValue string) []interface{} { row := make([]interface{}, 8) for index := range row { value := "" if index == 0 { value = firstValue } row[index] = excelize.Cell{StyleID: style, Value: value} } return row } func auditFilterDescription(filters auditFilters) string { rangeLabels := map[string]string{ "24h": "Past 24 hours", "7d": "Past 7 days", "30d": "Past 30 days", "90d": "Past 90 days", "all": "All retained events", } parts := []string{rangeLabels[filters.TimeRange]} filterValues := []struct { label string value string }{ {"event", filters.EventType}, {"customer", filters.CustomerLabel}, {"user", filters.User}, {"hostname", filters.Hostname}, {"package", filters.PackageSlug}, {"IP", filters.SourceIP}, {"details", filters.Details}, } for _, filter := range filterValues { if filter.value != "" { parts = append(parts, filter.label+": "+filter.value) } } return strings.Join(parts, " · ") }