` - return a + +func Report_main(datas []structure.Data, screenPath string) { + counts := map[string]int{} + techs := map[string]bool{} + for _, d := range datas { + counts[statusClass(d.Infos.Status_code)]++ + for _, t := range d.Infos.Technologies { + techs[t.Name] = true + } + } + + var b strings.Builder + b.WriteString(pageHead) + b.WriteString(renderSummary(len(datas), len(techs), counts)) + b.WriteString(`
`) + for _, d := range datas { + b.WriteString(card(d, screenPath)) + } + b.WriteString(pageFoot) + + _ = os.WriteFile("wappaGo_report.html", []byte(b.String()), 0644) } -func card(data structure.Data,screenPath string)(string){ - var technos string - var version string - var status string - for _,techno := range data.Infos.Technologies { - version ="" - if techno.Version != "" { - version=" | "+techno.Version +// renderSummary builds the header strip. It only ever interpolates integers and +// fixed band labels, so plain string assembly is safe (no user data). +func renderSummary(hosts, techCount int, counts map[string]int) string { + var bar, legend strings.Builder + for _, band := range statusBands { + c := counts[band.class] + if c == 0 { + continue } - technos = technos+""+techno.Name+version+"" + n := strconv.Itoa(c) + bar.WriteString(``) + legend.WriteString(`
  • ` + band.label + `` + n + `
  • `) } - status ="badge-secondary" - switch i := data.Infos.Status_code; { - case i<300: - status ="badge-success" - case i>299 && i<400: - status ="badge-info" - case i>399 && i<500: - status ="badge-warning" - case i>499: - status ="badge-danger" - } - return `
    -
    `+data.Url+`
    -
    - `+data.Infos.Title+` -
    -
    -
    `+data.Infos.Title+`
    -

    - Status: `+strconv.Itoa(data.Infos.Status_code)+` - `+technos+` -

    -
    - -
    ` + generated := time.Now().Format("2006-01-02 15:04 MST") + return `
    +
    ` + strconv.Itoa(hosts) + `hosts
    +
    + +
      ` + legend.String() + `
    +
    +
    ` + strconv.Itoa(techCount) + `technologies
    +
    ` + generated + `
    +
    ` } +const pageHead = ` + + + + +wappaGo · attack surface report + + + +
    +
    + + attack surface report +
    + +
    ` + +const pageFoot = `
    + + + +` diff --git a/report/report_test.go b/report/report_test.go new file mode 100644 index 0000000..f76e04e --- /dev/null +++ b/report/report_test.go @@ -0,0 +1,163 @@ +package report + +import ( + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/EasyRecon/wappaGo/structure" +) + +// TestCardEscapesXSS pins the report XSS fix: attacker-controlled fields (page +// title, scanned URL, detected technology names) must be HTML-escaped, so a +// malicious scanned site cannot inject script into the operator's report. +func TestCardEscapesXSS(t *testing.T) { + data := structure.Data{ + Url: "https://evil.test/", + Infos: structure.Host{ + Title: ``, + Status_code: 200, + Screenshot: "evil.png", + Technologies: []structure.Technologie{ + {Name: ``, Version: "1.0"}, + }, + }, + } + + out := card(data, "screens") + + if strings.Contains(out, "") { + t.Errorf("page title injected a raw `, IP: "203.0.113.9", + Technologies: []structure.Technologie{{Name: `Evil`}}, + }, + }, + } + + counts := map[string]int{} + techs := map[string]bool{} + for _, d := range datas { + counts[statusClass(d.Infos.Status_code)]++ + for _, te := range d.Infos.Technologies { + techs[te.Name] = true + } + } + var b strings.Builder + b.WriteString(pageHead) + b.WriteString(renderSummary(len(datas), len(techs), counts)) + b.WriteString(`
    `) + for _, d := range datas { + b.WriteString(card(d, "screenshots")) + } + b.WriteString(pageFoot) + page := b.String() + + for _, want := range []string{ + "", "", `id="lightbox"`, `id="filter"`, + "example.com", "93.184.216.34", "Cloudflare", "24.0 KB", "142 ms", "2.30 s", + "cert SANs", "*.example.com", "cname", `class="stamp">301`, `class="stamp">500`, + "https://www.example.com/shop", "1.21.0", + } { + if !strings.Contains(page, want) { + t.Errorf("rendered report is missing %q", want) + } + } + if strings.Contains(page, "") { + t.Errorf("full page leaked an unescaped