Path: blob/dev/pkg/reporting/exporters/markdown/markdown.go
2070 views
package markdown12import (3"bytes"4"os"5"path/filepath"6"strings"78"github.com/google/uuid"9"github.com/projectdiscovery/gologger"1011"github.com/projectdiscovery/nuclei/v3/pkg/output"12"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown/util"13"github.com/projectdiscovery/nuclei/v3/pkg/reporting/format"14fileutil "github.com/projectdiscovery/utils/file"15stringsutil "github.com/projectdiscovery/utils/strings"16)1718const indexFileName = "index.md"19const extension = ".md"2021type Exporter struct {22directory string23options *Options24}2526// Options contains the configuration options for GitHub issue tracker client27type Options struct {28// Directory is the directory to export found results to29Directory string `yaml:"directory"`30OmitRaw bool `yaml:"omit-raw"`31SortMode string `yaml:"sort-mode"`32}3334// New creates a new markdown exporter integration client based on options.35func New(options *Options) (*Exporter, error) {36directory := options.Directory37if options.Directory == "" {38dir, err := os.Getwd()39if err != nil {40return nil, err41}42directory = dir43}44_ = os.MkdirAll(directory, 0755)4546// index generation header47dataHeader := util.CreateTableHeader("Hostname/IP", "Finding", "Severity")4849err := os.WriteFile(filepath.Join(directory, indexFileName), []byte(dataHeader), 0644)50if err != nil {51return nil, err52}5354return &Exporter{options: options, directory: directory}, nil55}5657// Export exports a passed result event to markdown58func (exporter *Exporter) Export(event *output.ResultEvent) error {59// index file generation60file, err := os.OpenFile(filepath.Join(exporter.directory, indexFileName), os.O_APPEND|os.O_WRONLY, 0644)61if err != nil {62return err63}64defer func() {65_ = file.Close()66}()6768filename := createFileName(event)6970// If the sort mode is set to severity, host, or template, then we need to get a safe version of the name for a71// subdirectory to store the file in.72// This will allow us to sort the files into subdirectories based on the sort mode. The subdirectory will need to73// be created if it does not exist.74fileUrl := filename75subdirectory := ""76switch exporter.options.SortMode {77case "severity":78subdirectory = event.Info.SeverityHolder.Severity.String()79case "host":80subdirectory = event.Host81case "template":82subdirectory = event.TemplateID83}84if subdirectory != "" {85// Sanitize the subdirectory name to remove any characters that are not allowed in a directory name86subdirectory = sanitizeFilename(subdirectory)8788// Prepend the subdirectory name to the filename for the fileUrl89fileUrl = filepath.Join(subdirectory, filename)9091// Create the subdirectory if it does not exist92if err = fileutil.CreateFolders(filepath.Join(exporter.directory, subdirectory)); err != nil {93gologger.Warning().Msgf("Could not create subdirectory for markdown report: %s", err)94}95}9697host := util.CreateLink(event.Host, fileUrl)98finding := event.TemplateID + " " + event.MatcherName99severity := event.Info.SeverityHolder.Severity.String()100101_, err = file.WriteString(util.CreateTableRow(host, finding, severity))102if err != nil {103return err104}105106dataBuilder := &bytes.Buffer{}107dataBuilder.WriteString(util.CreateHeading3(format.Summary(event)))108dataBuilder.WriteString("\n")109dataBuilder.WriteString(util.CreateHorizontalLine())110dataBuilder.WriteString(format.CreateReportDescription(event, util.MarkdownFormatter{}, exporter.options.OmitRaw))111data := dataBuilder.Bytes()112113return os.WriteFile(filepath.Join(exporter.directory, subdirectory, filename), data, 0644)114}115116func createFileName(event *output.ResultEvent) string {117filenameBuilder := &strings.Builder{}118filenameBuilder.WriteString(event.TemplateID)119filenameBuilder.WriteString("-")120filenameBuilder.WriteString(event.Host)121filenameBuilder.WriteString("-")122filenameBuilder.WriteString(uuid.NewString())123124var suffix string125if event.MatcherName != "" {126suffix = event.MatcherName127} else if event.ExtractorName != "" {128suffix = event.ExtractorName129}130if suffix != "" {131filenameBuilder.WriteRune('-')132filenameBuilder.WriteString(event.MatcherName)133}134filenameBuilder.WriteString(extension)135return sanitizeFilename(filenameBuilder.String())136}137138// Close closes the exporter after operation139func (exporter *Exporter) Close() error {140return nil141}142143func sanitizeFilename(filename string) string {144if len(filename) > 256 {145filename = filename[0:255]146}147return stringsutil.ReplaceAll(filename, "_", "?", "/", ">", "|", ":", ";", "*", "<", "\"", "'", " ")148}149150151