2023-11-18 16:25:37 +05:30
|
|
|
package pdcp
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strconv"
|
|
|
|
|
"sync/atomic"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/projectdiscovery/gologger"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
|
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/output"
|
|
|
|
|
"github.com/projectdiscovery/retryablehttp-go"
|
2024-01-11 19:51:54 +05:30
|
|
|
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
|
2023-11-18 16:25:37 +05:30
|
|
|
errorutil "github.com/projectdiscovery/utils/errors"
|
|
|
|
|
fileutil "github.com/projectdiscovery/utils/file"
|
|
|
|
|
folderutil "github.com/projectdiscovery/utils/folder"
|
|
|
|
|
urlutil "github.com/projectdiscovery/utils/url"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
uploadEndpoint = "/v1/scans/import"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var _ output.Writer = &UploadWriter{}
|
|
|
|
|
|
|
|
|
|
// UploadWriter is a writer that uploads its output to pdcp
|
|
|
|
|
// server to enable web dashboard and more
|
|
|
|
|
type UploadWriter struct {
|
|
|
|
|
*output.StandardWriter
|
2024-01-11 19:51:54 +05:30
|
|
|
creds *pdcpauth.PDCPCredentials
|
2023-11-18 16:25:37 +05:30
|
|
|
tempFile *os.File
|
|
|
|
|
done atomic.Bool
|
|
|
|
|
uploadURL *url.URL
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewUploadWriter creates a new upload writer
|
2024-01-11 19:51:54 +05:30
|
|
|
func NewUploadWriter(creds *pdcpauth.PDCPCredentials) (*UploadWriter, error) {
|
2023-11-18 16:25:37 +05:30
|
|
|
if creds == nil {
|
|
|
|
|
return nil, fmt.Errorf("no credentials provided")
|
|
|
|
|
}
|
|
|
|
|
u := &UploadWriter{creds: creds}
|
|
|
|
|
// create a temporary file in cache directory
|
|
|
|
|
cacheDir := folderutil.AppCacheDirOrDefault("", config.BinaryName)
|
|
|
|
|
if !fileutil.FolderExists(cacheDir) {
|
|
|
|
|
_ = fileutil.CreateFolder(cacheDir)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var err error
|
|
|
|
|
// tempfile is created in nuclei-results-<unix-timestamp>.json format
|
|
|
|
|
u.tempFile, err = os.OpenFile(filepath.Join(cacheDir, "nuclei-results-"+strconv.Itoa(int(time.Now().Unix()))+".json"), os.O_RDWR|os.O_CREATE, 0600)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errorutil.NewWithErr(err).Msgf("could not create temporary file")
|
|
|
|
|
}
|
|
|
|
|
u.StandardWriter, err = output.NewWriter(
|
|
|
|
|
output.WithWriter(u.tempFile),
|
2023-11-30 22:00:28 +05:30
|
|
|
output.WithJson(true, true),
|
2023-11-18 16:25:37 +05:30
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errorutil.NewWithErr(err).Msgf("could not create output writer")
|
|
|
|
|
}
|
|
|
|
|
tmp, err := urlutil.Parse(creds.Server)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, errorutil.NewWithErr(err).Msgf("could not parse server url")
|
|
|
|
|
}
|
|
|
|
|
tmp.Path = uploadEndpoint
|
|
|
|
|
tmp.Update()
|
|
|
|
|
u.uploadURL = tmp.URL
|
|
|
|
|
return u, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type uploadResponse struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
Message string `json:"message"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Upload uploads the results to pdcp server
|
|
|
|
|
func (u *UploadWriter) Upload() {
|
|
|
|
|
defer u.done.Store(true)
|
|
|
|
|
|
2023-12-08 11:21:01 +05:30
|
|
|
_ = u.tempFile.Sync()
|
|
|
|
|
info, err := u.tempFile.Stat()
|
|
|
|
|
if err != nil {
|
|
|
|
|
gologger.Error().Msgf("Failed to upload scan results on cloud: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if info.Size() == 0 {
|
2023-11-18 16:25:37 +05:30
|
|
|
gologger.Verbose().Msgf("Scan results upload to cloud skipped, no results found to upload")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
_, _ = u.tempFile.Seek(0, 0)
|
|
|
|
|
|
|
|
|
|
id, err := u.upload()
|
|
|
|
|
if err != nil {
|
|
|
|
|
gologger.Error().Msgf("Failed to upload scan results on cloud: %v", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
gologger.Info().Msgf("Scan results uploaded! View them at %v", getScanDashBoardURL(id))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (u *UploadWriter) upload() (string, error) {
|
|
|
|
|
req, err := retryablehttp.NewRequest(http.MethodPost, u.uploadURL.String(), u.tempFile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", errorutil.NewWithErr(err).Msgf("could not create cloud upload request")
|
|
|
|
|
}
|
2024-01-11 19:51:54 +05:30
|
|
|
req.Header.Set(pdcpauth.ApiKeyHeaderName, u.creds.APIKey)
|
2023-11-18 16:25:37 +05:30
|
|
|
req.Header.Set("Content-Type", "application/octet-stream")
|
|
|
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
|
|
|
|
|
|
opts := retryablehttp.DefaultOptionsSingle
|
|
|
|
|
// we are uploading nuclei results which can be large
|
|
|
|
|
// server has a size limit of ~20ish MB
|
|
|
|
|
opts.Timeout = time.Duration(3) * time.Minute
|
|
|
|
|
client := retryablehttp.NewClient(opts)
|
|
|
|
|
resp, err := client.Do(req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", errorutil.NewWithErr(err).Msgf("could not upload results")
|
|
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
bin, err := io.ReadAll(resp.Body)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", errorutil.NewWithErr(err).Msgf("could not get id from response")
|
|
|
|
|
}
|
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
|
|
|
return "", fmt.Errorf("could not upload results got status code %v", resp.StatusCode)
|
|
|
|
|
}
|
|
|
|
|
var uploadResp uploadResponse
|
|
|
|
|
if err := json.Unmarshal(bin, &uploadResp); err != nil {
|
|
|
|
|
return "", errorutil.NewWithErr(err).Msgf("could not unmarshal response got %v", string(bin))
|
|
|
|
|
}
|
|
|
|
|
u.removeTempFile()
|
|
|
|
|
return uploadResp.ID, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// removeTempFile removes the temporary file
|
|
|
|
|
func (u *UploadWriter) removeTempFile() {
|
|
|
|
|
_ = os.Remove(u.tempFile.Name())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Close closes the upload writer
|
|
|
|
|
func (u *UploadWriter) Close() {
|
|
|
|
|
if !u.done.Load() {
|
|
|
|
|
u.Upload()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func getScanDashBoardURL(id string) string {
|
2024-01-11 19:51:54 +05:30
|
|
|
ux, _ := urlutil.Parse(pdcpauth.DashBoardURL)
|
2023-11-18 16:25:37 +05:30
|
|
|
ux.Path = "/scans/" + id
|
|
|
|
|
ux.Update()
|
|
|
|
|
return ux.String()
|
|
|
|
|
}
|