Files
Dogan Can Bakir 4311f8b736 Merge pull request #2363 from projectdiscovery/feature/database-output
feat: add database output support for storing scan results
2026-03-03 21:01:43 +03:00

214 lines
5.8 KiB
Go

package main
import (
"context"
"encoding/json"
"os"
"os/signal"
"runtime"
"runtime/pprof"
"github.com/logrusorgru/aurora"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/httpx/internal/db"
"github.com/projectdiscovery/httpx/internal/pdcp"
"github.com/projectdiscovery/httpx/runner"
pdcpauth "github.com/projectdiscovery/utils/auth/pdcp"
_ "github.com/projectdiscovery/utils/pprof"
)
func main() {
// Parse the command line flags and read config files
options := runner.ParseOptions()
// Profiling related code
if options.Memprofile != "" {
f, err := os.Create(options.Memprofile)
if err != nil {
gologger.Fatal().Msgf("profile: could not create memory profile %q: %v", options.Memprofile, err)
}
old := runtime.MemProfileRate
runtime.MemProfileRate = 4096
gologger.Print().Msgf("profile: memory profiling enabled (rate %d), %s", runtime.MemProfileRate, options.Memprofile)
defer func() {
_ = pprof.Lookup("heap").WriteTo(f, 0)
_ = f.Close()
runtime.MemProfileRate = old
gologger.Print().Msgf("profile: memory profiling disabled, %s", options.Memprofile)
}()
}
// validation for local results file upload
if options.AssetFileUpload != "" {
_ = setupOptionalAssetUpload(options)
file, err := os.Open(options.AssetFileUpload)
if err != nil {
gologger.Fatal().Msgf("Could not open file: %s\n", err)
}
defer func() {
_ = file.Close()
}()
dec := json.NewDecoder(file)
for dec.More() {
var r runner.Result
err := dec.Decode(&r)
if err != nil {
gologger.Fatal().Msgf("Could not decode jsonl file: %s\n", err)
}
options.OnResult(r)
}
options.OnClose()
return
}
// setup optional asset upload
_ = setupOptionalAssetUpload(options)
// setup optional database output
_ = setupDatabaseOutput(options)
httpxRunner, err := runner.New(options)
if err != nil {
gologger.Fatal().Msgf("Could not create runner: %s\n", err)
}
// Setup graceful exits
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
// First Ctrl+C: stop dispatching, let in-flight requests finish
<-c
gologger.Info().Msgf("CTRL+C pressed: Exiting\n")
httpxRunner.Interrupt()
// Second Ctrl+C: force exit
<-c
gologger.Info().Msgf("Forcing exit\n")
os.Exit(1)
}()
httpxRunner.RunEnumeration()
if httpxRunner.IsInterrupted() && options.ShouldSaveResume() {
gologger.Info().Msgf("Creating resume file: %s\n", runner.DefaultResumeFile)
err := httpxRunner.SaveResumeConfig()
if err != nil {
gologger.Error().Msgf("Couldn't create resume file: %s\n", err)
}
}
httpxRunner.Close()
}
// setupOptionalAssetUpload is used to setup optional asset upload
// this is optional and only initialized when explicitly enabled
func setupOptionalAssetUpload(opts *runner.Options) *pdcp.UploadWriter {
var mustEnable bool
// enable on multiple conditions
if opts.AssetUpload || opts.AssetID != "" || opts.AssetName != "" || pdcp.EnableCloudUpload {
mustEnable = true
}
a := aurora.NewAurora(!opts.NoColor)
if !mustEnable {
if !pdcp.HideAutoSaveMsg {
gologger.Print().Msgf("[%s] UI Dashboard is disabled, Use -dashboard option to enable", a.BrightYellow("WRN"))
}
return nil
}
gologger.Info().Msgf("To view results in UI dashboard, visit https://cloud.projectdiscovery.io/assets upon completion.")
h := &pdcpauth.PDCPCredHandler{}
creds, err := h.GetCreds()
if err != nil {
if err != pdcpauth.ErrNoCreds && !pdcp.HideAutoSaveMsg {
gologger.Verbose().Msgf("Could not get credentials for cloud upload: %s\n", err)
}
pdcpauth.CheckNValidateCredentials("httpx")
return nil
}
writer, err := pdcp.NewUploadWriterCallback(context.Background(), creds)
if err != nil {
gologger.Error().Msgf("failed to setup UI dashboard: %s", err)
return nil
}
if writer == nil {
gologger.Error().Msgf("something went wrong, could not setup UI dashboard")
}
opts.OnResult = writer.GetWriterCallback()
opts.OnClose = func() {
writer.Close()
}
// add additional metadata
if opts.AssetID != "" {
// silently ignore
writer.SetAssetID(opts.AssetID)
}
if opts.AssetName != "" {
// silently ignore
writer.SetAssetGroupName(opts.AssetName)
}
if opts.TeamID != "" {
writer.SetTeamID(opts.TeamID)
}
return writer
}
// setupDatabaseOutput sets up database output for storing results
// This is optional and only initialized when explicitly enabled via -rdb flag
func setupDatabaseOutput(opts *runner.Options) *db.Writer {
if !opts.ResultDatabase {
return nil
}
var cfg *db.Config
var err error
if opts.ResultDatabaseConfig != "" {
// Load configuration from file
cfg, err = db.LoadConfigFromFile(opts.ResultDatabaseConfig)
if err != nil {
gologger.Fatal().Msgf("Could not load database config: %s\n", err)
}
} else {
// Build configuration from CLI options
dbOpts := &db.Options{
Enabled: opts.ResultDatabase,
Type: opts.ResultDatabaseType,
ConnectionString: opts.ResultDatabaseConnStr,
DatabaseName: opts.ResultDatabaseName,
TableName: opts.ResultDatabaseTable,
BatchSize: opts.ResultDatabaseBatchSize,
OmitRaw: opts.ResultDatabaseOmitRaw,
}
cfg, err = dbOpts.ToConfig()
if err != nil {
gologger.Fatal().Msgf("Invalid database configuration: %s\n", err)
}
}
writer, err := db.NewWriter(context.Background(), cfg)
if err != nil {
gologger.Fatal().Msgf("Could not setup database output: %s\n", err)
}
// Chain with existing OnResult callback if present
existingCallback := opts.OnResult
opts.OnResult = func(r runner.Result) {
if existingCallback != nil {
existingCallback(r)
}
writer.GetWriterCallback()(r)
}
// Chain with existing OnClose callback if present
existingClose := opts.OnClose
opts.OnClose = func() {
writer.Close()
if existingClose != nil {
existingClose()
}
}
return writer
}