excel export update
This commit is contained in:
@@ -1,8 +1,11 @@
|
|||||||
module example.com/tapm/deployment-broker
|
module example.com/tapm/deployment-broker
|
||||||
|
|
||||||
go 1.24
|
go 1.24.0
|
||||||
|
|
||||||
require modernc.org/sqlite v1.38.2
|
require (
|
||||||
|
github.com/xuri/excelize/v2 v2.10.1
|
||||||
|
modernc.org/sqlite v1.38.2
|
||||||
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
@@ -10,8 +13,16 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/richardlehane/mscfb v1.0.6 // indirect
|
||||||
|
github.com/richardlehane/msoleps v1.0.6 // indirect
|
||||||
|
github.com/tiendc/go-deepcopy v1.7.2 // indirect
|
||||||
|
github.com/xuri/efp v0.0.1 // indirect
|
||||||
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect
|
||||||
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||||
golang.org/x/sys v0.34.0 // indirect
|
golang.org/x/net v0.50.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/text v0.34.0 // indirect
|
||||||
modernc.org/libc v1.66.3 // indirect
|
modernc.org/libc v1.66.3 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
@@ -8,19 +10,45 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/richardlehane/mscfb v1.0.6 h1:eN3bvvZCp00bs7Zf52bxNwAx5lJDBK1tCuH19qq5aC8=
|
||||||
|
github.com/richardlehane/mscfb v1.0.6/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo=
|
||||||
|
github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg=
|
||||||
|
github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44=
|
||||||
|
github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
|
||||||
|
github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
|
||||||
|
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
|
||||||
|
github.com/xuri/excelize/v2 v2.10.1 h1:V62UlqopMqha3kOpnlHy2CcRVw1V8E63jFoWUmMzxN0=
|
||||||
|
github.com/xuri/excelize/v2 v2.10.1/go.mod h1:iG5tARpgaEeIhTqt3/fgXCGoBRt4hNXgCp3tfXKoOIc=
|
||||||
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
|
||||||
|
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
golang.org/x/image v0.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
|
||||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
|
||||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||||
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
|
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||||
|
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM=
|
||||||
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||||
|
|||||||
+167
-349
@@ -1,16 +1,14 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
|
||||||
"compress/flate"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/crc32"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxAuditExportRows = 1_048_572
|
const maxAuditExportRows = 1_048_572
|
||||||
@@ -77,135 +75,67 @@ func writeAuditWorkbook(
|
|||||||
displayTimeZone = time.UTC
|
displayTimeZone = time.UTC
|
||||||
}
|
}
|
||||||
|
|
||||||
archive := zip.NewWriter(output)
|
workbook := excelize.NewFile()
|
||||||
files := []struct {
|
defer func() {
|
||||||
name string
|
if err := workbook.Close(); err != nil {
|
||||||
content func(io.Writer) error
|
log.Printf("close audit Excel workbook: %v", err)
|
||||||
}{
|
|
||||||
{"[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 {
|
const sheet = "Audit Log"
|
||||||
if err := writeRawZipFile(archive, file.name, file.content); err != nil {
|
if err := workbook.SetSheetName("Sheet1", sheet); err != nil {
|
||||||
_ = archive.Close()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
if err := workbook.SetDocProps(&excelize.DocProperties{
|
||||||
return archive.Close()
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeRawZipFile(
|
styles, err := newAuditWorkbookStyles(workbook)
|
||||||
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := content(io.MultiWriter(checksum, &uncompressedSize, sizeCompressor)); err != nil {
|
stream, err := workbook.NewStreamWriter(sheet)
|
||||||
_ = 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
compressor, err := flate.NewWriter(entry, flate.DefaultCompression)
|
if err := configureAuditWorksheet(stream); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := content(compressor); err != nil {
|
|
||||||
_ = compressor.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return compressor.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeAuditWorksheet(
|
if err := stream.SetRow(
|
||||||
w io.Writer,
|
"A1",
|
||||||
records []auditRecord,
|
styledAuditRow(styles.title, "TAPM Audit Log"),
|
||||||
filters auditFilters,
|
excelize.RowOpts{Height: 30},
|
||||||
exportedAt time.Time,
|
|
||||||
displayTimeZone *time.Location,
|
|
||||||
) error {
|
|
||||||
lastRow := len(records) + 4
|
|
||||||
if lastRow < 4 {
|
|
||||||
lastRow = 4
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := stream.MergeCell("A1", "H1"); err != nil {
|
||||||
if err := writeMergedRow(w, 1, "TAPM Audit Log", 1, 30); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata := fmt.Sprintf(
|
metadata := fmt.Sprintf(
|
||||||
"Exported %s · %s · %d event(s)",
|
"Exported %s · %s · %d event(s)",
|
||||||
exportedAt.In(displayTimeZone).Format("Jan 2, 2006 3:04 PM MST"),
|
exportedAt.In(displayTimeZone).Format("Jan 2, 2006 3:04 PM MST"),
|
||||||
auditFilterDescription(filters),
|
auditFilterDescription(filters),
|
||||||
len(records),
|
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
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,118 +149,17 @@ func writeAuditWorksheet(
|
|||||||
"Source IP",
|
"Source IP",
|
||||||
"Details",
|
"Details",
|
||||||
}
|
}
|
||||||
if _, err := writeString(w, `<row r="4" ht="24" customHeight="1">`); err != nil {
|
headerRow := make([]interface{}, len(headers))
|
||||||
return err
|
|
||||||
}
|
|
||||||
for index, header := range headers {
|
for index, header := range headers {
|
||||||
if err := writeInlineStringCell(w, cellReference(index+1, 4), header, 3); err != nil {
|
headerRow[index] = excelize.Cell{StyleID: styles.header, Value: header}
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
if err := stream.SetRow("A4", headerRow, excelize.RowOpts{Height: 24}); err != nil {
|
||||||
if _, err := writeString(w, `</row>`); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for index, record := range records {
|
for index, record := range records {
|
||||||
rowNumber := index + 5
|
rowNumber := index + 5
|
||||||
if _, err := fmt.Fprintf(w, `<row r="%d">`, rowNumber); err != nil {
|
local := record.CreatedAt.In(displayTimeZone)
|
||||||
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, `</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 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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, `</row>`)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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(
|
displayedTime := time.Date(
|
||||||
local.Year(),
|
local.Year(),
|
||||||
local.Month(),
|
local.Month(),
|
||||||
@@ -341,30 +170,143 @@ func writeDateCell(
|
|||||||
local.Nanosecond(),
|
local.Nanosecond(),
|
||||||
time.UTC,
|
time.UTC,
|
||||||
)
|
)
|
||||||
serial := float64(displayedTime.Unix())/86_400 + 25_569
|
row := []interface{}{
|
||||||
_, err := fmt.Fprintf(w, `<c r="%s" s="%d"><v>%.10f</v></c>`, reference, style, serial)
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func writeAuditCoreProperties(w io.Writer, exportedAt time.Time) error {
|
if len(records) > 0 {
|
||||||
if _, err := writeString(
|
lastRow := len(records) + 4
|
||||||
w,
|
showStripes := false
|
||||||
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`+
|
if err := stream.AddTable(&excelize.Table{
|
||||||
`<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" `+
|
Range: fmt.Sprintf("A4:H%d", lastRow),
|
||||||
`xmlns:dc="http://purl.org/dc/elements/1.1/" `+
|
Name: "AuditLog",
|
||||||
`xmlns:dcterms="http://purl.org/dc/terms/" `+
|
StyleName: "TableStyleMedium2",
|
||||||
`xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">`+
|
ShowRowStripes: &showStripes,
|
||||||
`<dc:title>TAPM Audit Log</dc:title><dc:creator>TAPM Broker</dc:creator>`+
|
ShowFirstColumn: false,
|
||||||
`<dcterms:created xsi:type="dcterms:W3CDTF">`,
|
ShowLastColumn: false,
|
||||||
); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := writeEscapedXML(w, exportedAt.UTC().Format(time.RFC3339)); err != nil {
|
}
|
||||||
|
if err := stream.Flush(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err := writeString(w, `</dcterms:created></cp:coreProperties>`)
|
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 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 {
|
func auditFilterDescription(filters auditFilters) string {
|
||||||
rangeLabels := map[string]string{
|
rangeLabels := map[string]string{
|
||||||
@@ -394,127 +336,3 @@ func auditFilterDescription(filters auditFilters) string {
|
|||||||
}
|
}
|
||||||
return strings.Join(parts, " · ")
|
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 = `<?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>`
|
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/xuri/excelize/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestWriteAuditWorkbookCreatesSafeExcelFile(t *testing.T) {
|
func TestWriteAuditWorkbookCreatesSafeExcelFile(t *testing.T) {
|
||||||
@@ -44,50 +44,38 @@ func TestWriteAuditWorkbookCreatesSafeExcelFile(t *testing.T) {
|
|||||||
t.Fatal("workbook is not a ZIP-based Excel file")
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
required := map[string]bool{
|
defer func() {
|
||||||
"[Content_Types].xml": false,
|
if err := workbook.Close(); err != nil {
|
||||||
"xl/workbook.xml": false,
|
t.Error(err)
|
||||||
"xl/styles.xml": false,
|
|
||||||
"xl/worksheets/sheet1.xml": false,
|
|
||||||
"xl/_rels/workbook.xml.rels": false,
|
|
||||||
}
|
}
|
||||||
var worksheet string
|
}()
|
||||||
for _, file := range archive.File {
|
if sheets := workbook.GetSheetList(); len(sheets) != 1 || sheets[0] != "Audit Log" {
|
||||||
if _, ok := required[file.Name]; ok {
|
t.Fatalf("sheets = %#v, want Audit Log", sheets)
|
||||||
required[file.Name] = true
|
|
||||||
}
|
}
|
||||||
if file.Name != "xl/worksheets/sheet1.xml" {
|
customer, err := workbook.GetCellValue("Audit Log", "C5")
|
||||||
continue
|
|
||||||
}
|
|
||||||
reader, err := file.Open()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
content, err := io.ReadAll(reader)
|
if customer != "Acme & Sons" {
|
||||||
_ = reader.Close()
|
t.Fatalf("customer = %q, want Acme & Sons", customer)
|
||||||
|
}
|
||||||
|
hostname, err := workbook.GetCellValue("Audit Log", "E5")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
worksheet = string(content)
|
if hostname != `=HYPERLINK("https://invalid.example")` {
|
||||||
|
t.Fatalf("hostname = %q, formula-looking text was not preserved", hostname)
|
||||||
}
|
}
|
||||||
for file, found := range required {
|
formula, err := workbook.GetCellFormula("Audit Log", "E5")
|
||||||
if !found {
|
if err != nil {
|
||||||
t.Errorf("workbook is missing %s", file)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
if formula != "" {
|
||||||
if !strings.Contains(worksheet, "Acme & Sons") {
|
t.Fatalf("untrusted audit text was written as formula %q", formula)
|
||||||
t.Fatal("worksheet text was not XML escaped")
|
|
||||||
}
|
|
||||||
if strings.Contains(worksheet, "<f>") {
|
|
||||||
t.Fatal("untrusted audit text must not be written as a formula")
|
|
||||||
}
|
|
||||||
if !strings.Contains(worksheet, `t="inlineStr"`) ||
|
|
||||||
!strings.Contains(worksheet, `=HYPERLINK`) {
|
|
||||||
t.Fatal("formula-looking audit text was not preserved as a text cell")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if samplePath := os.Getenv("TAPM_AUDIT_SAMPLE_PATH"); samplePath != "" {
|
if samplePath := os.Getenv("TAPM_AUDIT_SAMPLE_PATH"); samplePath != "" {
|
||||||
@@ -128,31 +116,26 @@ func TestAuditExportReturnsAllRowsAndRecordsDownload(t *testing.T) {
|
|||||||
t.Fatalf("content disposition = %q", disposition)
|
t.Fatalf("content disposition = %q", disposition)
|
||||||
}
|
}
|
||||||
|
|
||||||
archive, err := zip.NewReader(
|
workbook, err := excelize.OpenReader(bytes.NewReader(response.Body.Bytes()))
|
||||||
bytes.NewReader(response.Body.Bytes()),
|
|
||||||
int64(response.Body.Len()),
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var worksheet string
|
defer func() {
|
||||||
for _, file := range archive.File {
|
if err := workbook.Close(); err != nil {
|
||||||
if file.Name != "xl/worksheets/sheet1.xml" {
|
t.Error(err)
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
reader, err := file.Open()
|
}()
|
||||||
|
rows, err := workbook.GetRows("Audit Log")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
content, err := io.ReadAll(reader)
|
var exportedEvents []string
|
||||||
_ = reader.Close()
|
for _, row := range rows {
|
||||||
if err != nil {
|
if len(row) > 1 && (row[1] == "older_event" || row[1] == "newer_event") {
|
||||||
t.Fatal(err)
|
exportedEvents = append(exportedEvents, row[1])
|
||||||
}
|
}
|
||||||
worksheet = string(content)
|
|
||||||
}
|
}
|
||||||
if !strings.Contains(worksheet, "older_event") ||
|
if len(exportedEvents) != 2 {
|
||||||
!strings.Contains(worksheet, "newer_event") {
|
|
||||||
t.Fatal("complete export did not contain all retained audit rows")
|
t.Fatal("complete export did not contain all retained audit rows")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user