ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
web / web (push) Waiting to run
docs / links (push) Canceled after 0s
122 lines
3.0 KiB
Go
122 lines
3.0 KiB
Go
package server
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Autumn-27/artex/db"
|
|
"github.com/Autumn-27/artex/evidence"
|
|
"github.com/Autumn-27/artex/report"
|
|
)
|
|
|
|
func writeFindingsEvidenceZip(out io.Writer, findings []*db.DBFinding, stage string, now time.Time) error {
|
|
zw := zip.NewWriter(out)
|
|
if err := writeFindingsZipEntries(zw, findings, stage, now); err != nil {
|
|
zw.Close()
|
|
return err
|
|
}
|
|
return zw.Close()
|
|
}
|
|
|
|
func writeFindingsZipEntries(zw *zip.Writer, findings []*db.DBFinding, stage string, now time.Time) error {
|
|
store := evidence.New(nil, nil, stage) // private export copy; not subject to GC
|
|
used := map[string]int{}
|
|
for _, f := range findings {
|
|
base := report.FindingFilename(f)
|
|
name := base
|
|
if n := used[base]; n > 0 {
|
|
name = fmt.Sprintf("%s-%d.md", strings.TrimSuffix(base, ".md"), n+1)
|
|
}
|
|
used[base]++
|
|
w, err := zw.Create(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err = io.WriteString(w, report.SingleFindingMarkdown(f, now)); err != nil {
|
|
return err
|
|
}
|
|
for _, b := range f.TrafficBindings {
|
|
prefix := fmt.Sprintf("evidence/%d/%d/", f.ID, b.ID)
|
|
w, err := zw.Create(prefix + "manifest.json")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = json.NewEncoder(w).Encode(b); err != nil {
|
|
return err
|
|
}
|
|
for _, side := range []string{"request", "response"} {
|
|
if err := func() error {
|
|
body, _, err := store.OpenBody(b.Snapshot, side)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer body.Close()
|
|
w, err := zw.Create(prefix + side + ".http")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
head := b.Snapshot.ReqHead
|
|
if side == "response" {
|
|
head = b.Snapshot.RespHead
|
|
}
|
|
if _, err = io.WriteString(w, strings.TrimRight(head, "\r\n")+"\n\n"); err != nil {
|
|
return err
|
|
}
|
|
if _, err = io.Copy(w, body); err != nil {
|
|
return err
|
|
}
|
|
if _, err = body.Seek(0, io.SeekStart); err != nil {
|
|
return err
|
|
}
|
|
w, err = zw.Create(prefix + side + ".bin")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = io.Copy(w, body)
|
|
return err
|
|
}(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildFindingsEvidenceZip(path string, findings []*db.DBFinding, stage string, now time.Time) error {
|
|
f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writeErr := writeFindingsEvidenceZip(f, findings, stage, now)
|
|
syncErr := f.Sync()
|
|
closeErr := f.Close()
|
|
if err := errors.Join(writeErr, syncErr, closeErr); err != nil {
|
|
return err
|
|
}
|
|
// Re-read every compressed entry to validate the finished ZIP, including CRCs,
|
|
// before the caller starts an HTTP download response.
|
|
archive, err := zip.OpenReader(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer archive.Close()
|
|
for _, entry := range archive.File {
|
|
r, err := entry.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, readErr := io.Copy(io.Discard, r)
|
|
if err := errors.Join(readErr, r.Close()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|