Files
sif/internal/report/report_test.go
T
vmfunc c3a755f934 feat: live-host probe and sarif/markdown report export
adds an httpx-style -probe scanner reporting liveness, final status, page
title, server header and the redirect chain, plus -sarif/-markdown export
flags that serialize the collected run after the scan loop. the report
serializers live in a decoupled internal/report package consuming a raw-json
result model so they never import scan types.
2026-06-10 14:47:17 -07:00

173 lines
5.8 KiB
Go

/*
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
: :
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
: ▄█ █ █▀ · BSD 3-Clause License :
: :
: (c) 2022-2026 vmfunc, xyzeva, :
: lunchcat alumni & contributors :
: :
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
*/
package report
import (
"encoding/json"
"strings"
"testing"
)
// fakeResults are a couple of representative findings across two targets used by
// every test below.
func fakeResults() []Result {
return []Result{
{Target: "https://a.example.com", Module: "cors", Data: json.RawMessage(`{"severity":"high"}`)},
{Target: "https://a.example.com", Module: "probe", Data: json.RawMessage(`{"status_code":200}`)},
{Target: "https://b.example.com", Module: "redirect", Data: json.RawMessage(`{"parameter":"next"}`)},
}
}
func TestSARIF_ValidAndContainsFindings(t *testing.T) {
out, err := SARIF(fakeResults())
if err != nil {
t.Fatalf("SARIF: %v", err)
}
// the output must parse back into the sarif shape
var doc sarifLog
if err := json.Unmarshal(out, &doc); err != nil {
t.Fatalf("sarif output is not valid json: %v", err)
}
if doc.Version != "2.1.0" {
t.Errorf("expected sarif version 2.1.0, got %q", doc.Version)
}
if len(doc.Runs) != 1 {
t.Fatalf("expected exactly one run, got %d", len(doc.Runs))
}
run := doc.Runs[0]
if run.Tool.Driver.Name != "sif" {
t.Errorf("expected tool name sif, got %q", run.Tool.Driver.Name)
}
if len(run.Results) != 3 {
t.Fatalf("expected 3 results, got %d", len(run.Results))
}
// each finding's module id surfaces as the ruleId and its target as the uri
tests := []struct {
ruleID string
target string
}{
{"cors", "https://a.example.com"},
{"probe", "https://a.example.com"},
{"redirect", "https://b.example.com"},
}
for _, tt := range tests {
if !sarifHasResult(run.Results, tt.ruleID, tt.target) {
t.Errorf("expected sarif result rule=%q target=%q, got %+v", tt.ruleID, tt.target, run.Results)
}
}
// rules list each module id once, deduped across targets
if len(run.Tool.Driver.Rules) != 3 {
t.Errorf("expected 3 deduped rules, got %d: %+v", len(run.Tool.Driver.Rules), run.Tool.Driver.Rules)
}
}
func TestSARIF_DedupesRulesAcrossTargets(t *testing.T) {
// the same module on two targets must yield one rule but two results.
results := []Result{
{Target: "https://a.example.com", Module: "cors", Data: json.RawMessage(`{}`)},
{Target: "https://b.example.com", Module: "cors", Data: json.RawMessage(`{}`)},
}
out, err := SARIF(results)
if err != nil {
t.Fatalf("SARIF: %v", err)
}
var doc sarifLog
if err := json.Unmarshal(out, &doc); err != nil {
t.Fatalf("invalid json: %v", err)
}
run := doc.Runs[0]
if len(run.Tool.Driver.Rules) != 1 {
t.Errorf("expected 1 deduped rule, got %d", len(run.Tool.Driver.Rules))
}
if len(run.Results) != 2 {
t.Errorf("expected 2 results, got %d", len(run.Results))
}
}
func TestSARIF_Empty(t *testing.T) {
out, err := SARIF(nil)
if err != nil {
t.Fatalf("SARIF: %v", err)
}
var doc sarifLog
if err := json.Unmarshal(out, &doc); err != nil {
t.Fatalf("empty sarif is not valid json: %v", err)
}
if len(doc.Runs) != 1 {
t.Fatalf("expected one run even when empty, got %d", len(doc.Runs))
}
if len(doc.Runs[0].Results) != 0 {
t.Errorf("expected no results, got %d", len(doc.Runs[0].Results))
}
}
func TestMarkdown_ContainsTargetsAndModules(t *testing.T) {
out := string(Markdown(fakeResults()))
wants := []string{
"# sif scan report",
"## https://a.example.com",
"## https://b.example.com",
"### cors",
"### probe",
"### redirect",
`"severity": "high"`, // re-indented finding body
`"parameter": "next"`,
}
for _, want := range wants {
if !strings.Contains(out, want) {
t.Errorf("markdown report missing %q\n---\n%s", want, out)
}
}
}
func TestMarkdown_GroupsByTarget(t *testing.T) {
// a.example.com's two modules must both appear before b.example.com's header.
out := string(Markdown(fakeResults()))
aHeader := strings.Index(out, "## https://a.example.com")
bHeader := strings.Index(out, "## https://b.example.com")
if aHeader < 0 || bHeader < 0 {
t.Fatalf("missing target headers in:\n%s", out)
}
if aHeader > bHeader {
t.Errorf("expected target a before target b, got a=%d b=%d", aHeader, bHeader)
}
// both of a's modules sit between a's header and b's header
corsIdx := strings.Index(out, "### cors")
probeIdx := strings.Index(out, "### probe")
if corsIdx < aHeader || corsIdx > bHeader || probeIdx < aHeader || probeIdx > bHeader {
t.Errorf("expected a's modules grouped under a, cors=%d probe=%d (a=%d b=%d)", corsIdx, probeIdx, aHeader, bHeader)
}
}
// sarifHasResult reports whether any result carries the given rule id and target
// uri, the pairing that proves a finding survived serialization.
func sarifHasResult(results []sarifResult, ruleID, target string) bool {
for i := 0; i < len(results); i++ {
r := results[i]
if r.RuleID != ruleID {
continue
}
for j := 0; j < len(r.Locations); j++ {
if r.Locations[j].PhysicalLocation.ArtifactLocation.URI == target {
return true
}
}
}
return false
}