mirror of
https://github.com/lunchcat/sif.git
synced 2026-07-29 06:50:13 -07:00
* fix(report): sort sarif driver rules for deterministic output driver.rules was built by ranging a go map, so the emitted order varied between runs over the same input, producing byte-unstable sarif for identical scans. collect the rule ids into a slice and sort them before building the rules list; ruleId still references by id so no result is affected. * fix(report): strip crlf from markdown target headers target and module id are operator-supplied and were written verbatim into "## " / "### " heading lines, so a target containing an embedded newline followed by "## " text could inject a fake standalone heading into the markdown report. sanitizeHeading collapses cr/lf in both before they're written; the finding data block itself was already unaffected.
86 lines
3.2 KiB
Go
86 lines
3.2 KiB
Go
/*
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
: :
|
|
: █▀ █ █▀▀ · Blazing-fast pentesting suite :
|
|
: ▄█ █ █▀ · BSD 3-Clause License :
|
|
: :
|
|
: (c) 2022-2026 vmfunc, xyzeva, :
|
|
: lunchcat alumni & contributors :
|
|
: :
|
|
·━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━·
|
|
*/
|
|
|
|
package report
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Markdown renders results as a readable report grouped by target, then by
|
|
// module, with each module's finding pretty-printed as a json code block.
|
|
func Markdown(results []Result) []byte {
|
|
var b strings.Builder
|
|
b.WriteString("# sif scan report\n\n")
|
|
|
|
// group module results under their target so the report reads target-first
|
|
// regardless of the order results came in.
|
|
byTarget := make(map[string][]Result)
|
|
order := make([]string, 0)
|
|
for i := 0; i < len(results); i++ {
|
|
t := results[i].Target
|
|
if _, seen := byTarget[t]; !seen {
|
|
order = append(order, t)
|
|
}
|
|
byTarget[t] = append(byTarget[t], results[i])
|
|
}
|
|
|
|
for i := 0; i < len(order); i++ {
|
|
target := order[i]
|
|
b.WriteString("## ")
|
|
b.WriteString(sanitizeHeading(target))
|
|
b.WriteString("\n\n")
|
|
|
|
mods := byTarget[target]
|
|
// sort modules so the report is deterministic across runs
|
|
sort.SliceStable(mods, func(a, c int) bool { return mods[a].Module < mods[c].Module })
|
|
|
|
for j := 0; j < len(mods); j++ {
|
|
b.WriteString("### ")
|
|
b.WriteString(sanitizeHeading(mods[j].Module))
|
|
b.WriteString("\n\n")
|
|
b.WriteString("```json\n")
|
|
b.WriteString(prettyJSON(mods[j].Data))
|
|
b.WriteString("\n```\n\n")
|
|
}
|
|
}
|
|
|
|
return []byte(b.String())
|
|
}
|
|
|
|
// sanitizeHeading strips CR/LF from operator-supplied text (targets, module
|
|
// ids) before it's written into a markdown heading line. both target and
|
|
// module id can come from scan input, so an embedded newline followed by
|
|
// "## ..." would otherwise render as a standalone, injected heading.
|
|
func sanitizeHeading(s string) string {
|
|
s = strings.ReplaceAll(s, "\r\n", " ")
|
|
s = strings.ReplaceAll(s, "\r", " ")
|
|
s = strings.ReplaceAll(s, "\n", " ")
|
|
return s
|
|
}
|
|
|
|
// prettyJSON re-indents the raw finding for readability; if it doesn't parse as
|
|
// json (shouldn't happen, but never trust it) the raw bytes are returned as-is.
|
|
func prettyJSON(raw json.RawMessage) string {
|
|
if len(raw) == 0 {
|
|
return "null"
|
|
}
|
|
var indented bytes.Buffer
|
|
if err := json.Indent(&indented, raw, "", " "); err != nil {
|
|
return string(raw)
|
|
}
|
|
return indented.String()
|
|
}
|