Files
TA-Deployment-Broker/internal/app/audit_export.go
T
2026-07-28 22:26:17 -05:00

339 lines
8.4 KiB
Go

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(&timestampStyle)
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, " · ")
}