mirror of
https://github.com/lunchcat/sif.git
synced 2026-06-12 19:11:25 -07:00
c3a755f934
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.
173 lines
5.8 KiB
Go
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
|
|
}
|