339 lines
8.4 KiB
Go
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(×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, " · ")
|
|
}
|