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 = `
`