package app import ( "archive/zip" "compress/flate" "fmt" "hash/crc32" "io" "log" "net/http" "strings" "time" "unicode/utf8" ) 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 } archive := zip.NewWriter(output) files := []struct { name string content func(io.Writer) error }{ {"[Content_Types].xml", func(w io.Writer) error { return writeOnlyString(w, contentTypesXML) }}, {"_rels/.rels", func(w io.Writer) error { return writeOnlyString(w, packageRelationshipsXML) }}, {"docProps/app.xml", func(w io.Writer) error { return writeOnlyString(w, appPropertiesXML) }}, {"docProps/core.xml", func(w io.Writer) error { return writeAuditCoreProperties(w, exportedAt) }}, {"xl/workbook.xml", func(w io.Writer) error { return writeOnlyString(w, workbookXML) }}, {"xl/_rels/workbook.xml.rels", func(w io.Writer) error { return writeOnlyString(w, workbookRelationshipsXML) }}, {"xl/styles.xml", func(w io.Writer) error { return writeOnlyString(w, auditStylesXML) }}, {"xl/worksheets/sheet1.xml", func(w io.Writer) error { return writeAuditWorksheet(w, records, filters, exportedAt, displayTimeZone) }}, } for _, file := range files { if err := writeRawZipFile(archive, file.name, file.content); err != nil { _ = archive.Close() return err } } return archive.Close() } func writeRawZipFile( archive *zip.Writer, name string, content func(io.Writer) error, ) error { checksum := crc32.NewIEEE() var compressedSize byteCounter var uncompressedSize byteCounter sizeCompressor, err := flate.NewWriter(&compressedSize, flate.DefaultCompression) if err != nil { return err } if err := content(io.MultiWriter(checksum, &uncompressedSize, sizeCompressor)); err != nil { _ = sizeCompressor.Close() return err } if err := sizeCompressor.Close(); err != nil { return err } header := &zip.FileHeader{ Name: name, Method: zip.Deflate, CRC32: checksum.Sum32(), CompressedSize64: uint64(compressedSize), UncompressedSize64: uint64(uncompressedSize), } entry, err := archive.CreateRaw(header) if err != nil { return err } compressor, err := flate.NewWriter(entry, flate.DefaultCompression) if err != nil { return err } if err := content(compressor); err != nil { _ = compressor.Close() return err } return compressor.Close() } func writeAuditWorksheet( w io.Writer, records []auditRecord, filters auditFilters, exportedAt time.Time, displayTimeZone *time.Location, ) error { lastRow := len(records) + 4 if lastRow < 4 { lastRow = 4 } if _, err := fmt.Fprintf( w, ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``+ ``, lastRow, ); err != nil { return err } if err := writeMergedRow(w, 1, "TAPM Audit Log", 1, 30); 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 := writeMergedRow(w, 2, metadata, 2, 24); err != nil { return err } if _, err := writeString(w, ``); err != nil { return err } headers := []string{ "Timestamp", "Event", "Customer / Deployment", "User", "Hostname", "Package ID", "Source IP", "Details", } if _, err := writeString(w, ``); err != nil { return err } for index, header := range headers { if err := writeInlineStringCell(w, cellReference(index+1, 4), header, 3); err != nil { return err } } if _, err := writeString(w, ``); err != nil { return err } for index, record := range records { rowNumber := index + 5 if _, err := fmt.Fprintf(w, ``, rowNumber); err != nil { return err } if err := writeDateCell( w, cellReference(1, rowNumber), record.CreatedAt, displayTimeZone, 5, ); err != nil { return err } values := []string{ record.EventType, record.CustomerLabel, record.User, record.Hostname, record.PackageSlug, record.SourceIP, record.Details, } for column, value := range values { style := 4 if column == len(values)-1 { style = 6 } if err := writeInlineStringCell( w, cellReference(column+2, rowNumber), value, style, ); err != nil { return err } } if _, err := writeString(w, ``); err != nil { return err } } if _, err := fmt.Fprintf( w, ``+ ``+ ``+ ``+ ``, lastRow, ); err != nil { return err } return nil } func writeMergedRow(w io.Writer, row int, value string, style int, height int) error { if _, err := fmt.Fprintf(w, ``, row, height); err != nil { return err } for column := 1; column <= 8; column++ { cellValue := "" if column == 1 { cellValue = value } if err := writeInlineStringCell(w, cellReference(column, row), cellValue, style); err != nil { return err } } _, err := writeString(w, ``) return err } func writeInlineStringCell(w io.Writer, reference string, value string, style int) error { if _, err := fmt.Fprintf(w, ``); err != nil { return err } if err := writeEscapedXML(w, value); err != nil { return err } _, err := writeString(w, ``) return err } func writeDateCell( w io.Writer, reference string, value time.Time, displayTimeZone *time.Location, style int, ) error { local := value.In(displayTimeZone) displayedTime := time.Date( local.Year(), local.Month(), local.Day(), local.Hour(), local.Minute(), local.Second(), local.Nanosecond(), time.UTC, ) serial := float64(displayedTime.Unix())/86_400 + 25_569 _, err := fmt.Fprintf(w, `%.10f`, reference, style, serial) return err } func writeAuditCoreProperties(w io.Writer, exportedAt time.Time) error { if _, err := writeString( w, ``+ ``+ `TAPM Audit LogTAPM Broker`+ ``, ); err != nil { return err } if err := writeEscapedXML(w, exportedAt.UTC().Format(time.RFC3339)); err != nil { return err } _, err := writeString(w, ``) return err } 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, " · ") } func cellReference(column int, row int) string { var letters [3]byte position := len(letters) for column > 0 { column-- position-- letters[position] = byte('A' + column%26) column /= 26 } return string(letters[position:]) + fmt.Sprintf("%d", row) } func writeEscapedXML(w io.Writer, value string) error { value = strings.ToValidUTF8(value, "\uFFFD") for _, character := range value { if character == '\t' || character == '\n' || character == '\r' || (character >= 0x20 && character <= 0xD7FF) || (character >= 0xE000 && character <= 0xFFFD) || (character >= 0x10000 && character <= utf8.MaxRune) { switch character { case '&': if _, err := writeString(w, "&"); err != nil { return err } case '<': if _, err := writeString(w, "<"); err != nil { return err } case '>': if _, err := writeString(w, ">"); err != nil { return err } case '"': if _, err := writeString(w, """); err != nil { return err } case '\'': if _, err := writeString(w, "'"); err != nil { return err } default: if _, err := fmt.Fprint(w, string(character)); err != nil { return err } } } } return nil } func writeString(w io.Writer, value string) (int, error) { return io.WriteString(w, value) } func writeOnlyString(w io.Writer, value string) error { _, err := io.WriteString(w, value) return err } const contentTypesXML = ` ` const packageRelationshipsXML = ` ` const appPropertiesXML = ` TAPM Broker ` const workbookXML = ` ` const workbookRelationshipsXML = ` ` const auditStylesXML = ` `