mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-17 17:35:28 +00:00
* chore: fix non-constant fmt string in call Signed-off-by: Dwi Siswanto <git@dw1.io> * build: bump all direct modules Signed-off-by: Dwi Siswanto <git@dw1.io> * chore(hosterrorscache): update import path Signed-off-by: Dwi Siswanto <git@dw1.io> * fix(charts): break changes Signed-off-by: Dwi Siswanto <git@dw1.io> * build: pinned `github.com/zmap/zcrypto` to v0.0.0-20240512203510-0fef58d9a9db Signed-off-by: Dwi Siswanto <git@dw1.io> * chore: golangci-lint auto fixes Signed-off-by: Dwi Siswanto <git@dw1.io> * chore: satisfy lints Signed-off-by: Dwi Siswanto <git@dw1.io> * build: migrate `github.com/xanzy/go-gitlab` => `gitlab.com/gitlab-org/api/client-go` Signed-off-by: Dwi Siswanto <git@dw1.io> * feat(json): update build constraints Signed-off-by: Dwi Siswanto <git@dw1.io> * chore: dont panicking on close err Signed-off-by: Dwi Siswanto <git@dw1.io> --------- Signed-off-by: Dwi Siswanto <git@dw1.io>
150 lines
4.4 KiB
Go
150 lines
4.4 KiB
Go
package markdown
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/projectdiscovery/gologger"
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown/util"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/format"
|
|
fileutil "github.com/projectdiscovery/utils/file"
|
|
stringsutil "github.com/projectdiscovery/utils/strings"
|
|
)
|
|
|
|
const indexFileName = "index.md"
|
|
const extension = ".md"
|
|
|
|
type Exporter struct {
|
|
directory string
|
|
options *Options
|
|
}
|
|
|
|
// Options contains the configuration options for GitHub issue tracker client
|
|
type Options struct {
|
|
// Directory is the directory to export found results to
|
|
Directory string `yaml:"directory"`
|
|
OmitRaw bool `yaml:"omit-raw"`
|
|
SortMode string `yaml:"sort-mode"`
|
|
}
|
|
|
|
// New creates a new markdown exporter integration client based on options.
|
|
func New(options *Options) (*Exporter, error) {
|
|
directory := options.Directory
|
|
if options.Directory == "" {
|
|
dir, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
directory = dir
|
|
}
|
|
_ = os.MkdirAll(directory, 0755)
|
|
|
|
// index generation header
|
|
dataHeader := util.CreateTableHeader("Hostname/IP", "Finding", "Severity")
|
|
|
|
err := os.WriteFile(filepath.Join(directory, indexFileName), []byte(dataHeader), 0644)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &Exporter{options: options, directory: directory}, nil
|
|
}
|
|
|
|
// Export exports a passed result event to markdown
|
|
func (exporter *Exporter) Export(event *output.ResultEvent) error {
|
|
// index file generation
|
|
file, err := os.OpenFile(filepath.Join(exporter.directory, indexFileName), os.O_APPEND|os.O_WRONLY, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
_ = file.Close()
|
|
}()
|
|
|
|
filename := createFileName(event)
|
|
|
|
// If the sort mode is set to severity, host, or template, then we need to get a safe version of the name for a
|
|
// subdirectory to store the file in.
|
|
// This will allow us to sort the files into subdirectories based on the sort mode. The subdirectory will need to
|
|
// be created if it does not exist.
|
|
fileUrl := filename
|
|
subdirectory := ""
|
|
switch exporter.options.SortMode {
|
|
case "severity":
|
|
subdirectory = event.Info.SeverityHolder.Severity.String()
|
|
case "host":
|
|
subdirectory = event.Host
|
|
case "template":
|
|
subdirectory = event.TemplateID
|
|
}
|
|
if subdirectory != "" {
|
|
// Sanitize the subdirectory name to remove any characters that are not allowed in a directory name
|
|
subdirectory = sanitizeFilename(subdirectory)
|
|
|
|
// Prepend the subdirectory name to the filename for the fileUrl
|
|
fileUrl = filepath.Join(subdirectory, filename)
|
|
|
|
// Create the subdirectory if it does not exist
|
|
if err = fileutil.CreateFolders(filepath.Join(exporter.directory, subdirectory)); err != nil {
|
|
gologger.Warning().Msgf("Could not create subdirectory for markdown report: %s", err)
|
|
}
|
|
}
|
|
|
|
host := util.CreateLink(event.Host, fileUrl)
|
|
finding := event.TemplateID + " " + event.MatcherName
|
|
severity := event.Info.SeverityHolder.Severity.String()
|
|
|
|
_, err = file.WriteString(util.CreateTableRow(host, finding, severity))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dataBuilder := &bytes.Buffer{}
|
|
dataBuilder.WriteString(util.CreateHeading3(format.Summary(event)))
|
|
dataBuilder.WriteString("\n")
|
|
dataBuilder.WriteString(util.CreateHorizontalLine())
|
|
dataBuilder.WriteString(format.CreateReportDescription(event, util.MarkdownFormatter{}, exporter.options.OmitRaw))
|
|
data := dataBuilder.Bytes()
|
|
|
|
return os.WriteFile(filepath.Join(exporter.directory, subdirectory, filename), data, 0644)
|
|
}
|
|
|
|
func createFileName(event *output.ResultEvent) string {
|
|
filenameBuilder := &strings.Builder{}
|
|
filenameBuilder.WriteString(event.TemplateID)
|
|
filenameBuilder.WriteString("-")
|
|
filenameBuilder.WriteString(event.Host)
|
|
filenameBuilder.WriteString("-")
|
|
filenameBuilder.WriteString(uuid.NewString())
|
|
|
|
var suffix string
|
|
if event.MatcherName != "" {
|
|
suffix = event.MatcherName
|
|
} else if event.ExtractorName != "" {
|
|
suffix = event.ExtractorName
|
|
}
|
|
if suffix != "" {
|
|
filenameBuilder.WriteRune('-')
|
|
filenameBuilder.WriteString(event.MatcherName)
|
|
}
|
|
filenameBuilder.WriteString(extension)
|
|
return sanitizeFilename(filenameBuilder.String())
|
|
}
|
|
|
|
// Close closes the exporter after operation
|
|
func (exporter *Exporter) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func sanitizeFilename(filename string) string {
|
|
if len(filename) > 256 {
|
|
filename = filename[0:255]
|
|
}
|
|
return stringsutil.ReplaceAll(filename, "_", "?", "/", ">", "|", ":", ";", "*", "<", "\"", "'", " ")
|
|
}
|