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") +}