diff --git a/parsers/burp/xml/strings.go b/parsers/burp/xml/strings.go
new file mode 100644
index 0000000..f07d702
--- /dev/null
+++ b/parsers/burp/xml/strings.go
@@ -0,0 +1,136 @@
+package burpxml
+
+import "fmt"
+
+func (i Item) ToStrings(noReq, noResp bool) []string {
+ arr := []string{
+ i.Time,
+ i.URL,
+ i.Host.Name,
+ i.Host.IP,
+ i.Port,
+ i.Protocol,
+ i.Path,
+ i.Extension,
+ }
+
+ if !noReq {
+ arr = append(arr, i.Request.ToStrings()...)
+ }
+
+ arr = append(arr, []string{
+ i.Status,
+ i.ResponseLength,
+ i.MimeType,
+ }...)
+
+ if !noResp {
+ arr = append(arr, i.Response.ToStrings()...)
+ }
+
+ arr = append(arr, i.Comment)
+ return arr
+}
+
+func (r Request) ToStrings() []string {
+ if r.Body != "" {
+ return []string{r.Body}
+ }
+ return []string{r.Base64Encoded, r.Raw}
+}
+
+func (r Response) ToStrings() []string {
+ if r.Body != "" {
+ return []string{r.Body}
+ }
+ return []string{r.Base64Encoded, r.Raw}
+}
+
+func (i Item) FlatString() string {
+ return fmt.Sprintf(`%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s`,
+ i.Time,
+ i.URL,
+ i.Host.Name,
+ i.Host.IP,
+ i.Port,
+ i.Protocol,
+ i.Path,
+ i.Extension,
+ i.Request.FlatString(),
+ i.Status,
+ i.ResponseLength,
+ i.MimeType,
+ i.Response.FlatString(),
+ i.Comment,
+ )
+}
+
+func (r Request) FlatString() string {
+ if r.Body != "" {
+ return r.Body
+ }
+ return fmt.Sprintf(`%s,%s`, r.Base64Encoded, r.Raw)
+}
+
+func (r Response) FlatString() string {
+ if r.Body != "" {
+ return r.Body
+ }
+ return fmt.Sprintf(`%s,%s`, r.Base64Encoded, r.Raw)
+}
+
+func (i Item) String() string {
+ return fmt.Sprintf(`Item{
+ Time = %s,
+ Url = %s,
+ Host = %s,
+ IP = %s,
+ Port = %s,
+ Proto = %s,
+ Path = %s,
+ Ext = %s,
+ %s,
+ Status = %s,
+ RespLen = %s,
+ MIME = %s,
+ %s,
+ Comment = %s,
+}`,
+ i.Time,
+ i.URL,
+ i.Host.Name,
+ i.Host.IP,
+ i.Port,
+ i.Protocol,
+ i.Path,
+ i.Extension,
+ i.Request.String(),
+ i.Status,
+ i.ResponseLength,
+ i.MimeType,
+ i.Response.String(),
+ i.Comment,
+ )
+}
+
+func (r Request) String() string {
+ s := "Request{\n"
+ if r.Body != "" {
+ s += fmt.Sprintf(" Body = %s,\n", r.Body)
+ } else {
+ s += fmt.Sprintf(" Base64 = %s,\nBody = %s,\n", r.Base64Encoded, r.Raw)
+ }
+ s += "}"
+ return s
+}
+
+func (r Response) String() string {
+ s := "Response{\n"
+ if r.Body != "" {
+ s += fmt.Sprintf(" Body = %s,\n", r.Body)
+ } else {
+ s += fmt.Sprintf(" Base64 = %s,\nBody = %s,\n", r.Base64Encoded, r.Raw)
+ }
+ s += "}"
+ return s
+}
diff --git a/parsers/burp/xml/types.go b/parsers/burp/xml/types.go
new file mode 100644
index 0000000..181f2b3
--- /dev/null
+++ b/parsers/burp/xml/types.go
@@ -0,0 +1,113 @@
+package burpxml
+
+import (
+ "encoding/csv"
+ "encoding/json"
+ "fmt"
+ "io"
+)
+
+type Request struct {
+ Base64Encoded string `xml:"base64,attr" json:"base64_encoded,omitempty"`
+ Raw string `xml:",chardata" json:"raw,omitempty"`
+ Body string `xml:"-" json:"body,omitempty"`
+}
+
+type Response struct {
+ Base64Encoded string `xml:"base64,attr" json:"base64_encoded,omitempty"`
+ Raw string `xml:",chardata" json:"raw,omitempty"`
+ Body string `xml:"-" json:"body,omitempty"`
+}
+
+type Host struct {
+ IP string `xml:"ip,attr" json:"ip,omitempty"`
+ Name string `xml:",chardata" json:"name,omitempty"`
+}
+
+type Item struct {
+ Time string `xml:"time" json:"time,omitempty"`
+ URL string `xml:"url" json:"url,omitempty"`
+ Host Host `xml:"host" json:"host,omitzero"`
+ Port string `xml:"port" json:"port,omitempty"`
+ Protocol string `xml:"protocol" json:"protocol,omitempty"`
+ Path string `xml:"path" json:"path,omitempty"`
+ Extension string `xml:"extension" json:"extension,omitempty"`
+ Request Request `xml:"request" json:"request,omitzero"`
+ Status string `xml:"status" json:"status,omitempty"`
+ ResponseLength string `xml:"responselength" json:"response_length,omitempty"`
+ MimeType string `xml:"mimetype" json:"mime_type,omitempty"`
+ Response Response `xml:"response" json:"response,omitzero"`
+ Comment string `xml:"comment" json:"comment,omitempty"`
+}
+
+type Items struct {
+ Items []Item `xml:"item" json:"items,omitempty"`
+}
+
+func (items *Items) ToJSON(w io.Writer) error {
+ enc := json.NewEncoder(w)
+ enc.SetIndent("", " ")
+ if err := enc.Encode(items); err != nil {
+ return fmt.Errorf("json encode: %w", err)
+ }
+ return nil
+}
+
+type CSVOptions struct {
+ ExcludeRequest bool
+ ExcludeResponse bool
+}
+
+func (items *Items) ToCSV(w io.Writer, opts CSVOptions) error {
+ enc := csv.NewWriter(w)
+ defer enc.Flush()
+
+ for _, item := range items.Items {
+ record := item.toRecord(opts)
+ if err := enc.Write(record); err != nil {
+ return fmt.Errorf("csv write: %w", err)
+ }
+ }
+
+ return enc.Error()
+}
+
+func (item *Item) toRecord(opts CSVOptions) []string {
+ record := []string{
+ item.Time,
+ item.URL,
+ item.Host.Name,
+ item.Host.IP,
+ item.Port,
+ item.Protocol,
+ item.Path,
+ item.Extension,
+ }
+
+ if !opts.ExcludeRequest {
+ record = append(record, item.Request.content())
+ }
+
+ record = append(record, item.Status, item.ResponseLength, item.MimeType)
+
+ if !opts.ExcludeResponse {
+ record = append(record, item.Response.content())
+ }
+
+ record = append(record, item.Comment)
+ return record
+}
+
+func (r *Request) content() string {
+ if r.Body != "" {
+ return r.Body
+ }
+ return r.Raw
+}
+
+func (r *Response) content() string {
+ if r.Body != "" {
+ return r.Body
+ }
+ return r.Raw
+}
diff --git a/parsers/burp/xml/xml.go b/parsers/burp/xml/xml.go
new file mode 100644
index 0000000..f1732fc
--- /dev/null
+++ b/parsers/burp/xml/xml.go
@@ -0,0 +1,70 @@
+package burpxml
+
+import (
+ "encoding/base64"
+ "encoding/xml"
+ "fmt"
+ "io"
+ "strconv"
+)
+
+type XMLParseOptions struct {
+ DecodeBase64 bool
+}
+
+func ParseXML(r io.Reader, opts XMLParseOptions) (*Items, error) {
+ var items Items
+ if err := xml.NewDecoder(r).Decode(&items); err != nil {
+ return nil, fmt.Errorf("xml decode: %w", err)
+ }
+
+ if !opts.DecodeBase64 {
+ return &items, nil
+ }
+
+ for i := range items.Items {
+ if err := decodeItemBodies(&items.Items[i]); err != nil {
+ return nil, err
+ }
+ }
+
+ return &items, nil
+}
+
+func decodeItemBodies(item *Item) error {
+ decoded, err := decodeBase64Field(item.Request.Base64Encoded, item.Request.Raw)
+ if err != nil {
+ return fmt.Errorf("decode request: %w", err)
+ }
+ item.Request.Body = decoded
+
+ decoded, err = decodeBase64Field(item.Response.Base64Encoded, item.Response.Raw)
+ if err != nil {
+ return fmt.Errorf("decode response: %w", err)
+ }
+ item.Response.Body = decoded
+
+ return nil
+}
+
+func decodeBase64Field(base64Flag, raw string) (string, error) {
+ if base64Flag == "" {
+ return raw, nil
+ }
+
+ isBase64, err := strconv.ParseBool(base64Flag)
+ if err != nil {
+ return "", fmt.Errorf("parse base64 flag: %w", err)
+ }
+
+ if !isBase64 {
+ return raw, nil
+ }
+
+ decoded, err := base64.StdEncoding.DecodeString(raw)
+ if err != nil {
+ return "", fmt.Errorf("base64 decode: %w", err)
+ }
+
+ return string(decoded), nil
+}
diff --git a/parsers/burp/xml/xml_test.go b/parsers/burp/xml/xml_test.go
new file mode 100644
index 0000000..4964def
--- /dev/null
+++ b/parsers/burp/xml/xml_test.go
@@ -0,0 +1,344 @@
+package burpxml
+
+import (
+ "bytes"
+ "encoding/base64"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+const testXML = `
+
+ -
+
+ https://example.com/api/users
+ example.com
+ 443
+ https
+ /api/users
+ json
+ GET /api/users HTTP/1.1
+Host: example.com
+
+
+ 200
+ 1234
+ application/json
+ HTTP/1.1 200 OK
+Content-Type: application/json
+
+{"users":[]}
+ Test request
+
+`
+
+func TestParseXML(t *testing.T) {
+ items, err := ParseXML(strings.NewReader(testXML), XMLParseOptions{})
+ require.NoError(t, err)
+ require.Len(t, items.Items, 1)
+
+ item := items.Items[0]
+ require.Equal(t, "Mon Jan 01 12:00:00 UTC 2024", item.Time)
+ require.Equal(t, "https://example.com/api/users", item.URL)
+ require.Equal(t, "example.com", item.Host.Name)
+ require.Equal(t, "93.184.216.34", item.Host.IP)
+ require.Equal(t, "443", item.Port)
+ require.Equal(t, "https", item.Protocol)
+ require.Equal(t, "/api/users", item.Path)
+ require.Equal(t, "json", item.Extension)
+ require.Equal(t, "200", item.Status)
+ require.Equal(t, "1234", item.ResponseLength)
+ require.Equal(t, "application/json", item.MimeType)
+ require.Equal(t, "Test request", item.Comment)
+ require.Contains(t, item.Request.Raw, "GET /api/users HTTP/1.1")
+ require.Contains(t, item.Response.Raw, "HTTP/1.1 200 OK")
+}
+
+func TestParseXMLWithBase64Decode(t *testing.T) {
+ reqBody := "GET /secret HTTP/1.1\nHost: test.com\n"
+ respBody := "HTTP/1.1 200 OK\n\nSecret data"
+
+ xml := `
+
+ -
+
+ https://test.com/secret
+ test.com
+ 443
+ https
+ /secret
+
+ ` + base64.StdEncoding.EncodeToString([]byte(reqBody)) + `
+ 200
+ 100
+ text/plain
+ ` + base64.StdEncoding.EncodeToString([]byte(respBody)) + `
+
+
+`
+
+ items, err := ParseXML(strings.NewReader(xml), XMLParseOptions{DecodeBase64: true})
+ require.NoError(t, err)
+ require.Len(t, items.Items, 1)
+
+ item := items.Items[0]
+ require.Equal(t, reqBody, item.Request.Body)
+ require.Equal(t, respBody, item.Response.Body)
+}
+
+func TestParseXMLInvalidXML(t *testing.T) {
+ _, err := ParseXML(strings.NewReader("
+
+ -
+
+
+
+
+
+
+
+ not-valid-base64!!!
+
+
+
+
+
+
+`
+
+ _, err := ParseXML(strings.NewReader(xml), XMLParseOptions{DecodeBase64: true})
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "base64 decode")
+}
+
+func TestParseXMLMultipleItems(t *testing.T) {
+ xml := `
+
+ -
+
+ https://a.com
+ a.com
+ 443
+ https
+ /a
+
+ req1
+ 200
+ 10
+ text/html
+ resp1
+
+
+ -
+
+ https://b.com
+ b.com
+ 80
+ http
+ /b
+
+ req2
+ 404
+ 20
+ text/plain
+ resp2
+
+
+`
+
+ items, err := ParseXML(strings.NewReader(xml), XMLParseOptions{})
+ require.NoError(t, err)
+ require.Len(t, items.Items, 2)
+
+ require.Equal(t, "https://a.com", items.Items[0].URL)
+ require.Equal(t, "https://b.com", items.Items[1].URL)
+ require.Equal(t, "200", items.Items[0].Status)
+ require.Equal(t, "404", items.Items[1].Status)
+}
+
+func TestParseXMLEmptyItems(t *testing.T) {
+ xml := ``
+ items, err := ParseXML(strings.NewReader(xml), XMLParseOptions{})
+ require.NoError(t, err)
+ require.Empty(t, items.Items)
+}
+
+func TestItemsToJSON(t *testing.T) {
+ items := &Items{
+ Items: []Item{
+ {
+ URL: "https://example.com",
+ Host: Host{Name: "example.com", IP: "1.2.3.4"},
+ Port: "443",
+ Protocol: "https",
+ Status: "200",
+ },
+ },
+ }
+
+ var buf bytes.Buffer
+ err := items.ToJSON(&buf)
+ require.NoError(t, err)
+
+ json := buf.String()
+ require.Contains(t, json, `"url": "https://example.com"`)
+ require.Contains(t, json, `"status": "200"`)
+}
+
+func TestItemsToCSV(t *testing.T) {
+ items := &Items{
+ Items: []Item{
+ {
+ Time: "Time1",
+ URL: "https://example.com",
+ Host: Host{Name: "example.com", IP: "1.2.3.4"},
+ Port: "443",
+ Protocol: "https",
+ Path: "/path",
+ Extension: "html",
+ Request: Request{Raw: "GET / HTTP/1.1"},
+ Status: "200",
+ ResponseLength: "100",
+ MimeType: "text/html",
+ Response: Response{Raw: "HTTP/1.1 200 OK"},
+ Comment: "test",
+ },
+ },
+ }
+
+ var buf bytes.Buffer
+ err := items.ToCSV(&buf, CSVOptions{})
+ require.NoError(t, err)
+
+ csv := buf.String()
+ require.Contains(t, csv, "Time1")
+ require.Contains(t, csv, "https://example.com")
+ require.Contains(t, csv, "GET / HTTP/1.1")
+}
+
+func TestItemsToCSVExcludeRequestResponse(t *testing.T) {
+ items := &Items{
+ Items: []Item{
+ {
+ URL: "https://example.com",
+ Request: Request{Raw: "SECRET_REQUEST"},
+ Response: Response{Raw: "SECRET_RESPONSE"},
+ },
+ },
+ }
+
+ var buf bytes.Buffer
+ err := items.ToCSV(&buf, CSVOptions{ExcludeRequest: true, ExcludeResponse: true})
+ require.NoError(t, err)
+
+ csv := buf.String()
+ require.NotContains(t, csv, "SECRET_REQUEST")
+ require.NotContains(t, csv, "SECRET_RESPONSE")
+}
+
+func TestRequestContent(t *testing.T) {
+ t.Run("returns body when set", func(t *testing.T) {
+ r := &Request{Raw: "raw", Body: "decoded"}
+ require.Equal(t, "decoded", r.content())
+ })
+
+ t.Run("returns raw when body empty", func(t *testing.T) {
+ r := &Request{Raw: "raw"}
+ require.Equal(t, "raw", r.content())
+ })
+}
+
+func TestResponseContent(t *testing.T) {
+ t.Run("returns body when set", func(t *testing.T) {
+ r := &Response{Raw: "raw", Body: "decoded"}
+ require.Equal(t, "decoded", r.content())
+ })
+
+ t.Run("returns raw when body empty", func(t *testing.T) {
+ r := &Response{Raw: "raw"}
+ require.Equal(t, "raw", r.content())
+ })
+}
+
+func TestItemString(t *testing.T) {
+ item := Item{URL: "https://example.com", Host: Host{Name: "example.com"}, Status: "200"}
+ s := item.String()
+ require.Contains(t, s, "https://example.com")
+ require.Contains(t, s, "example.com")
+ require.Contains(t, s, "200")
+ require.Contains(t, s, "Item{")
+}
+
+func TestRequestString(t *testing.T) {
+ t.Run("shows body when decoded", func(t *testing.T) {
+ r := Request{Raw: "raw", Body: "decoded body"}
+ s := r.String()
+ require.Contains(t, s, "Request{")
+ require.Contains(t, s, "Body = decoded body")
+ })
+
+ t.Run("shows base64 and raw when not decoded", func(t *testing.T) {
+ r := Request{Base64Encoded: "true", Raw: "raw content"}
+ s := r.String()
+ require.Contains(t, s, "Request{")
+ require.Contains(t, s, "Base64")
+ require.Contains(t, s, "raw content")
+ })
+}
+
+func TestResponseString(t *testing.T) {
+ t.Run("shows body when decoded", func(t *testing.T) {
+ r := Response{Raw: "raw", Body: "decoded body"}
+ s := r.String()
+ require.Contains(t, s, "Response{")
+ require.Contains(t, s, "Body = decoded body")
+ })
+
+ t.Run("shows base64 and raw when not decoded", func(t *testing.T) {
+ r := Response{Base64Encoded: "false", Raw: "raw content"}
+ s := r.String()
+ require.Contains(t, s, "Response{")
+ require.Contains(t, s, "Base64")
+ require.Contains(t, s, "raw content")
+ })
+}
+
+func TestItemToStrings(t *testing.T) {
+ item := Item{
+ Time: "time",
+ URL: "url",
+ Host: Host{Name: "host", IP: "ip"},
+ Port: "port",
+ Protocol: "proto",
+ Path: "path",
+ Extension: "ext",
+ Request: Request{Body: "req"},
+ Status: "200",
+ ResponseLength: "100",
+ MimeType: "text",
+ Response: Response{Body: "resp"},
+ Comment: "comment",
+ }
+
+ strs := item.ToStrings(false, false)
+ require.Contains(t, strs, "time")
+ require.Contains(t, strs, "url")
+ require.Contains(t, strs, "host")
+ require.Contains(t, strs, "req")
+ require.Contains(t, strs, "resp")
+}
+
+func TestItemFlatString(t *testing.T) {
+ item := Item{URL: "https://example.com", Status: "200"}
+ s := item.FlatString()
+ require.Contains(t, s, "https://example.com")
+ require.Contains(t, s, "200")
+}