Dwi Siswanto 87ed0b2bb9
build: bump all direct modules (#6290)
* 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>
2025-07-01 00:40:44 +07:00

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, "_", "?", "/", ">", "|", ":", ";", "*", "<", "\"", "'", " ")
}