excel export update

This commit is contained in:
2026-07-28 22:26:17 -05:00
parent 5c4268524b
commit bf5d626fe8
4 changed files with 266 additions and 426 deletions
+175 -357
View File
@@ -1,16 +1,14 @@
package app
import (
"archive/zip"
"compress/flate"
"fmt"
"hash/crc32"
"io"
"log"
"net/http"
"strings"
"time"
"unicode/utf8"
"github.com/xuri/excelize/v2"
)
const maxAuditExportRows = 1_048_572
@@ -77,135 +75,67 @@ func writeAuditWorkbook(
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
workbook := excelize.NewFile()
defer func() {
if err := workbook.Close(); err != nil {
log.Printf("close audit Excel workbook: %v", 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 {
const sheet = "Audit Log"
if err := workbook.SetSheetName("Sheet1", sheet); err != nil {
return err
}
if err := content(io.MultiWriter(checksum, &uncompressedSize, sizeCompressor)); err != nil {
_ = sizeCompressor.Close()
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
}
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
styles, err := newAuditWorkbookStyles(workbook)
if err != nil {
return err
}
if _, err := fmt.Fprintf(
w,
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`+
`<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">`+
`<dimension ref="A1:H%d"/>`+
`<sheetViews><sheetView showGridLines="0" workbookViewId="0">`+
`<pane ySplit="4" topLeftCell="A5" activePane="bottomLeft" state="frozen"/>`+
`</sheetView></sheetViews>`+
`<sheetFormatPr defaultRowHeight="15"/>`+
`<cols>`+
`<col min="1" max="1" width="22" customWidth="1"/>`+
`<col min="2" max="2" width="28" customWidth="1"/>`+
`<col min="3" max="3" width="30" customWidth="1"/>`+
`<col min="4" max="4" width="22" customWidth="1"/>`+
`<col min="5" max="5" width="24" customWidth="1"/>`+
`<col min="6" max="6" width="24" customWidth="1"/>`+
`<col min="7" max="7" width="18" customWidth="1"/>`+
`<col min="8" max="8" width="54" customWidth="1"/>`+
`</cols><sheetData>`,
lastRow,
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 := writeMergedRow(w, 1, "TAPM Audit Log", 1, 30); err != nil {
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 := writeMergedRow(w, 2, metadata, 2, 24); err != nil {
if err := stream.SetRow(
"A2",
styledAuditRow(styles.metadata, metadata),
excelize.RowOpts{Height: 24},
); err != nil {
return err
}
if _, err := writeString(w, `<row r="3" ht="8" customHeight="1"/>`); err != nil {
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
}
@@ -219,151 +149,163 @@ func writeAuditWorksheet(
"Source IP",
"Details",
}
if _, err := writeString(w, `<row r="4" ht="24" customHeight="1">`); err != nil {
return err
}
headerRow := make([]interface{}, len(headers))
for index, header := range headers {
if err := writeInlineStringCell(w, cellReference(index+1, 4), header, 3); err != nil {
return err
}
headerRow[index] = excelize.Cell{StyleID: styles.header, Value: header}
}
if _, err := writeString(w, `</row>`); err != nil {
if err := stream.SetRow("A4", headerRow, excelize.RowOpts{Height: 24}); err != nil {
return err
}
for index, record := range records {
rowNumber := index + 5
if _, err := fmt.Fprintf(w, `<row r="%d">`, rowNumber); err != nil {
return err
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 := 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, `</row>`); err != nil {
if err := stream.SetRow(fmt.Sprintf("A%d", rowNumber), row); err != nil {
return err
}
}
if _, err := fmt.Fprintf(
w,
`</sheetData><autoFilter ref="A4:H%d"/>`+
`<mergeCells count="2"><mergeCell ref="A1:H1"/><mergeCell ref="A2:H2"/></mergeCells>`+
`<pageMargins left="0.25" right="0.25" top="0.5" bottom="0.5" header="0.2" footer="0.2"/>`+
`<pageSetup orientation="landscape" fitToWidth="1" fitToHeight="0"/>`+
`</worksheet>`,
lastRow,
); err != nil {
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 nil
return workbook.Write(output)
}
func writeMergedRow(w io.Writer, row int, value string, style int, height int) error {
if _, err := fmt.Fprintf(w, `<row r="%d" ht="%d" customHeight="1">`, row, height); err != nil {
return err
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
}
for column := 1; column <= 8; column++ {
cellValue := ""
if column == 1 {
cellValue = value
}
if err := writeInlineStringCell(w, cellReference(column, row), cellValue, style); err != nil {
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
}
}
_, err := writeString(w, `</row>`)
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 writeInlineStringCell(w io.Writer, reference string, value string, style int) error {
if _, err := fmt.Fprintf(w, `<c r="%s" s="%d" t="inlineStr"><is><t`, reference, style); err != nil {
return err
}
if strings.HasPrefix(value, " ") || strings.HasSuffix(value, " ") {
if _, err := writeString(w, ` xml:space="preserve"`); err != nil {
return err
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}
}
if _, err := writeString(w, `>`); err != nil {
return err
}
if err := writeEscapedXML(w, value); err != nil {
return err
}
_, err := writeString(w, `</t></is></c>`)
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, `<c r="%s" s="%d"><v>%.10f</v></c>`, reference, style, serial)
return err
}
func writeAuditCoreProperties(w io.Writer, exportedAt time.Time) error {
if _, err := writeString(
w,
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`+
`<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" `+
`xmlns:dc="http://purl.org/dc/elements/1.1/" `+
`xmlns:dcterms="http://purl.org/dc/terms/" `+
`xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">`+
`<dc:title>TAPM Audit Log</dc:title><dc:creator>TAPM Broker</dc:creator>`+
`<dcterms:created xsi:type="dcterms:W3CDTF">`,
); err != nil {
return err
}
if err := writeEscapedXML(w, exportedAt.UTC().Format(time.RFC3339)); err != nil {
return err
}
_, err := writeString(w, `</dcterms:created></cp:coreProperties>`)
return err
return row
}
func auditFilterDescription(filters auditFilters) string {
@@ -394,127 +336,3 @@ func auditFilterDescription(filters auditFilters) string {
}
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, "&amp;"); err != nil {
return err
}
case '<':
if _, err := writeString(w, "&lt;"); err != nil {
return err
}
case '>':
if _, err := writeString(w, "&gt;"); err != nil {
return err
}
case '"':
if _, err := writeString(w, "&quot;"); err != nil {
return err
}
case '\'':
if _, err := writeString(w, "&apos;"); 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 = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>
<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>
</Types>`
const packageRelationshipsXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
</Relationships>`
const appPropertiesXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
<Application>TAPM Broker</Application>
</Properties>`
const workbookXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheets><sheet name="Audit Log" sheetId="1" r:id="rId1"/></sheets>
</workbook>`
const workbookRelationshipsXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
</Relationships>`
const auditStylesXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
<numFmts count="1"><numFmt numFmtId="164" formatCode="yyyy-mm-dd hh:mm:ss"/></numFmts>
<fonts count="3">
<font><sz val="11"/><color rgb="FF33404A"/><name val="Aptos"/></font>
<font><b/><sz val="18"/><color rgb="FFFFFFFF"/><name val="Aptos Display"/></font>
<font><b/><sz val="11"/><color rgb="FFFFFFFF"/><name val="Aptos"/></font>
</fonts>
<fills count="4">
<fill><patternFill patternType="none"/></fill>
<fill><patternFill patternType="gray125"/></fill>
<fill><patternFill patternType="solid"><fgColor rgb="FF33404A"/><bgColor indexed="64"/></patternFill></fill>
<fill><patternFill patternType="solid"><fgColor rgb="FFF4F7ED"/><bgColor indexed="64"/></patternFill></fill>
</fills>
<borders count="2">
<border><left/><right/><top/><bottom/><diagonal/></border>
<border><left/><right/><top/><bottom style="thin"><color rgb="FFD9DED4"/></bottom><diagonal/></border>
</borders>
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
<cellXfs count="7">
<xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/>
<xf numFmtId="0" fontId="1" fillId="2" borderId="0" xfId="0" applyFont="1" applyFill="1"><alignment vertical="center"/></xf>
<xf numFmtId="0" fontId="0" fillId="3" borderId="0" xfId="0" applyFill="1"><alignment vertical="center"/></xf>
<xf numFmtId="0" fontId="2" fillId="2" borderId="0" xfId="0" applyFont="1" applyFill="1"><alignment vertical="center"/></xf>
<xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" applyBorder="1"><alignment vertical="top"/></xf>
<xf numFmtId="164" fontId="0" fillId="0" borderId="1" xfId="0" applyNumberFormat="1" applyBorder="1"><alignment vertical="top"/></xf>
<xf numFmtId="0" fontId="0" fillId="0" borderId="1" xfId="0" applyBorder="1"><alignment vertical="top" wrapText="1"/></xf>
</cellXfs>
<cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0"/></cellStyles>
</styleSheet>`
+41 -58
View File
@@ -1,15 +1,15 @@
package app
import (
"archive/zip"
"bytes"
"io"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"time"
"github.com/xuri/excelize/v2"
)
func TestWriteAuditWorkbookCreatesSafeExcelFile(t *testing.T) {
@@ -44,50 +44,38 @@ func TestWriteAuditWorkbookCreatesSafeExcelFile(t *testing.T) {
t.Fatal("workbook is not a ZIP-based Excel file")
}
archive, err := zip.NewReader(bytes.NewReader(output.Bytes()), int64(output.Len()))
workbook, err := excelize.OpenReader(bytes.NewReader(output.Bytes()))
if err != nil {
t.Fatal(err)
}
required := map[string]bool{
"[Content_Types].xml": false,
"xl/workbook.xml": false,
"xl/styles.xml": false,
"xl/worksheets/sheet1.xml": false,
"xl/_rels/workbook.xml.rels": false,
}
var worksheet string
for _, file := range archive.File {
if _, ok := required[file.Name]; ok {
required[file.Name] = true
defer func() {
if err := workbook.Close(); err != nil {
t.Error(err)
}
if file.Name != "xl/worksheets/sheet1.xml" {
continue
}
reader, err := file.Open()
if err != nil {
t.Fatal(err)
}
content, err := io.ReadAll(reader)
_ = reader.Close()
if err != nil {
t.Fatal(err)
}
worksheet = string(content)
}()
if sheets := workbook.GetSheetList(); len(sheets) != 1 || sheets[0] != "Audit Log" {
t.Fatalf("sheets = %#v, want Audit Log", sheets)
}
for file, found := range required {
if !found {
t.Errorf("workbook is missing %s", file)
}
customer, err := workbook.GetCellValue("Audit Log", "C5")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(worksheet, "Acme &amp; Sons") {
t.Fatal("worksheet text was not XML escaped")
if customer != "Acme & Sons" {
t.Fatalf("customer = %q, want Acme & Sons", customer)
}
if strings.Contains(worksheet, "<f>") {
t.Fatal("untrusted audit text must not be written as a formula")
hostname, err := workbook.GetCellValue("Audit Log", "E5")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(worksheet, `t="inlineStr"`) ||
!strings.Contains(worksheet, `=HYPERLINK`) {
t.Fatal("formula-looking audit text was not preserved as a text cell")
if hostname != `=HYPERLINK("https://invalid.example")` {
t.Fatalf("hostname = %q, formula-looking text was not preserved", hostname)
}
formula, err := workbook.GetCellFormula("Audit Log", "E5")
if err != nil {
t.Fatal(err)
}
if formula != "" {
t.Fatalf("untrusted audit text was written as formula %q", formula)
}
if samplePath := os.Getenv("TAPM_AUDIT_SAMPLE_PATH"); samplePath != "" {
@@ -128,31 +116,26 @@ func TestAuditExportReturnsAllRowsAndRecordsDownload(t *testing.T) {
t.Fatalf("content disposition = %q", disposition)
}
archive, err := zip.NewReader(
bytes.NewReader(response.Body.Bytes()),
int64(response.Body.Len()),
)
workbook, err := excelize.OpenReader(bytes.NewReader(response.Body.Bytes()))
if err != nil {
t.Fatal(err)
}
var worksheet string
for _, file := range archive.File {
if file.Name != "xl/worksheets/sheet1.xml" {
continue
defer func() {
if err := workbook.Close(); err != nil {
t.Error(err)
}
reader, err := file.Open()
if err != nil {
t.Fatal(err)
}
content, err := io.ReadAll(reader)
_ = reader.Close()
if err != nil {
t.Fatal(err)
}
worksheet = string(content)
}()
rows, err := workbook.GetRows("Audit Log")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(worksheet, "older_event") ||
!strings.Contains(worksheet, "newer_event") {
var exportedEvents []string
for _, row := range rows {
if len(row) > 1 && (row[1] == "older_event" || row[1] == "newer_event") {
exportedEvents = append(exportedEvents, row[1])
}
}
if len(exportedEvents) != 2 {
t.Fatal("complete export did not contain all retained audit rows")
}