87 | >(({ className, ...props }, ref) => (
88 | | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/cmd/scan_cidr.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/sensepost/gowitness/internal/ascii"
7 | "github.com/sensepost/gowitness/internal/islazy"
8 | "github.com/sensepost/gowitness/pkg/log"
9 | "github.com/sensepost/gowitness/pkg/readers"
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | var cidrCmdOptions = &readers.CidrReaderOptions{}
14 | var cidrCmd = &cobra.Command{
15 | Use: "cidr",
16 | Short: "Scan CIDR targets on a network",
17 | Long: ascii.LogoHelp(ascii.Markdown(`
18 | # scan cidr
19 |
20 | Scan CIDR targets on a network.
21 |
22 | This command takes input CIDR ranges, optional extra ports, and other
23 | configuration options to generate permutations for scanning web services to screenshot.
24 | URL schemes are automatically added as 'http://' and 'https://' unless either
25 | the --no-http or --no-https flags are present.
26 |
27 | By default, this command will scan targets sequentially. If the --random flag is
28 | set, targets will go through a shuffling phase before scanning starts. This is
29 | useful in cases where scanning too many ports in sequence may trigger port
30 | scanning-related alerts.
31 |
32 | **Note**: By default, no metadata is saved except for screenshots that are
33 | stored in the configured --screenshot-path. For later parsing (i.e., using the
34 | gowitness reporting feature), you need to specify where to write results (db,
35 | csv, jsonl) using the _--write-*_ set of flags. See _--help_ for available
36 | flags.`)),
37 | Example: ascii.Markdown(`
38 | - gowitness scan cidr --cidr 192.168.0.0/24 --cidr 10.0.50.0/24
39 | - gowitness scan cidr -c 10.0.50.0/24 --port 8888 --port 8443
40 | - gowitness scan cidr -c 172.16.1.0/24 -c 10.10.10.0/24 --no-http --ports-medium
41 | - gowitness scan cidr -t 20 --log-scan-errors -c 10.20.20.0/28`),
42 | PreRunE: func(cmd *cobra.Command, args []string) error {
43 | if cidrCmdOptions.Source == "" && len(cidrCmdOptions.Cidrs) == 0 {
44 | return errors.New("need targets to scan via either a --cidr-file or --cidr")
45 | }
46 |
47 | if cidrCmdOptions.Source != "" && !islazy.FileExists(cidrCmdOptions.Source) {
48 | return errors.New("source is not readable")
49 | }
50 |
51 | return nil
52 | },
53 | Run: func(cmd *cobra.Command, args []string) {
54 | log.Debug("starting CIDR scanning", "file", cidrCmdOptions.Source, "cidrs", cidrCmdOptions.Cidrs)
55 |
56 | reader := readers.NewCidrReader(cidrCmdOptions)
57 | go func() {
58 | if err := reader.Read(scanRunner.Targets); err != nil {
59 | log.Error("error in reader.Read", "err", err)
60 | return
61 | }
62 | }()
63 |
64 | scanRunner.Run()
65 | scanRunner.Close()
66 | },
67 | }
68 |
69 | func init() {
70 | scanCmd.AddCommand(cidrCmd)
71 |
72 | cidrCmd.Flags().StringVarP(&cidrCmdOptions.Source, "cidr-file", "f", "", "A file with target CIDRs to scan. Use - for stdin")
73 | cidrCmd.Flags().BoolVar(&cidrCmdOptions.NoHTTP, "no-http", false, "Do not add 'http://' to targets where missing")
74 | cidrCmd.Flags().BoolVar(&cidrCmdOptions.NoHTTPS, "no-https", false, "Do not add 'https://' to targets where missing")
75 | cidrCmd.Flags().StringSliceVarP(&cidrCmdOptions.Cidrs, "cidr", "c", []string{}, "A network CIDR to scan. Supports multiple --cidr flags")
76 | cidrCmd.Flags().IntSliceVarP(&cidrCmdOptions.Ports, "port", "p", []int{80, 443}, "Ports on targets to scan. Supports multiple --port flags")
77 | cidrCmd.Flags().BoolVar(&cidrCmdOptions.PortsSmall, "ports-small", false, "Include a small ports list when scanning targets")
78 | cidrCmd.Flags().BoolVar(&cidrCmdOptions.PortsMedium, "ports-medium", false, "Include a medium ports list when scanning targets")
79 | cidrCmd.Flags().BoolVar(&cidrCmdOptions.PortsLarge, "ports-large", false, "Include a large ports list when scanning targets")
80 | cidrCmd.Flags().BoolVar(&cidrCmdOptions.Random, "random", false, "Randomize scan targets")
81 | }
82 |
--------------------------------------------------------------------------------
/cmd/scan_nessus.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/sensepost/gowitness/internal/ascii"
7 | "github.com/sensepost/gowitness/internal/islazy"
8 | "github.com/sensepost/gowitness/pkg/log"
9 | "github.com/sensepost/gowitness/pkg/readers"
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | var nessusCmdOptions = &readers.NessusReaderOptions{}
14 | var nessusCmd = &cobra.Command{
15 | Use: "nessus",
16 | Short: "Scan targets from a Nessus XML file",
17 | Long: ascii.LogoHelp(ascii.Markdown(`
18 | # scan nessus
19 |
20 | Scan targets from a Nessus XML file.
21 |
22 | Targets are parsed out of an exported Nessus scan result in XML format. This
23 | format is typically called "Nessus" format in the export menu.
24 |
25 | By default, the parser will search for web services using the following rules:
26 | - Plugin Name Contains: "Service Detection"
27 | - Plugin Service Name Contains: "www" or "http"
28 | - Plugin Output Value Contains: "web server"
29 |
30 | With these defaults, the parser should detect most web services from a Nessus
31 | scan export. You can adjust the filters to include more Plugin Names, Service
32 | Names, or Plugin Output filters using the --service-name, --plugin-output, and
33 | --plugin-name flags.
34 |
35 | Including the --hostnames flag will have the parser add a scan target based on
36 | any hostname information found in a matched result.
37 |
38 | **Note**: By default, no metadata is saved except for screenshots that are
39 | stored in the configured --screenshot-path. For later parsing (i.e., using the
40 | gowitness reporting feature), you need to specify where to write results (db,
41 | csv, jsonl) using the _--write-*_ set of flags. See _--help_ for available
42 | flags.`)),
43 | Example: ascii.Markdown(`
44 | - gowitness scan nessus -f ~/Desktop/scan-results.nessus
45 | - gowitness scan nessus -f results.nessus --threads 50 --plugin-output server
46 | - gowitness scan nessus -f ./scan-results.nessus --port 80`),
47 | PreRunE: func(cmd *cobra.Command, args []string) error {
48 | if nessusCmdOptions.Source == "" {
49 | return errors.New("a source must be specified")
50 | }
51 |
52 | if !islazy.FileExists(nessusCmdOptions.Source) {
53 | return errors.New("source is not readable")
54 | }
55 |
56 | return nil
57 | },
58 | Run: func(cmd *cobra.Command, args []string) {
59 | log.Debug("starting Nessus file scanning", "file", nessusCmdOptions.Source)
60 |
61 | reader := readers.NewNessusReader(nessusCmdOptions)
62 | go func() {
63 | if err := reader.Read(scanRunner.Targets); err != nil {
64 | log.Error("error in reader.Read", "err", err)
65 | return
66 | }
67 | }()
68 |
69 | scanRunner.Run()
70 | scanRunner.Close()
71 | },
72 | }
73 |
74 | func init() {
75 | scanCmd.AddCommand(nessusCmd)
76 |
77 | nessusCmd.Flags().StringVarP(&nessusCmdOptions.Source, "file", "f", "", "A file with targets to scan. Use - for stdin")
78 | nessusCmd.Flags().BoolVar(&nessusCmdOptions.NoHTTP, "no-http", false, "Do not add 'http://' to targets where missing")
79 | nessusCmd.Flags().BoolVar(&nessusCmdOptions.NoHTTPS, "no-https", false, "Do not add 'https://' to targets where missing")
80 | nessusCmd.Flags().BoolVar(&nessusCmdOptions.Hostnames, "hostnames", false, "Enable hostname scanning")
81 | nessusCmd.Flags().StringSliceVar(&nessusCmdOptions.Services, "service-name", []string{"www", "http"}, "Service name filter. Supports multiple --service-name flags")
82 | nessusCmd.Flags().StringSliceVar(&nessusCmdOptions.PluginOutputs, "plugin-output", []string{"web server"}, "Plugin output contains filter. Supports multiple --plugin-output flags")
83 | nessusCmd.Flags().StringSliceVar(&nessusCmdOptions.PluginNames, "plugin-name", []string{"Service Detection"}, "Plugin name filter. Supports multiple --plugin-name flags")
84 | nessusCmd.Flags().IntSliceVar(&nessusCmdOptions.Ports, "port", []int{}, "Port filter. Supports multiple --port flags")
85 | }
86 |
--------------------------------------------------------------------------------
/web/api/submit.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "log/slog"
6 | "net/http"
7 |
8 | "github.com/sensepost/gowitness/pkg/log"
9 | "github.com/sensepost/gowitness/pkg/runner"
10 | driver "github.com/sensepost/gowitness/pkg/runner/drivers"
11 | "github.com/sensepost/gowitness/pkg/writers"
12 | )
13 |
14 | type submitRequest struct {
15 | URLs []string `json:"urls"`
16 | Options *submitRequestOptions `json:"options"`
17 | }
18 |
19 | type submitRequestOptions struct {
20 | X int `json:"window_x"`
21 | Y int `json:"window_y"`
22 | UserAgent string `json:"user_agent"`
23 | Timeout int `json:"timeout"`
24 | Delay int `json:"delay"`
25 | Format string `json:"format"`
26 | }
27 |
28 | // SubmitHandler submits URL's for scans, writing them to the database.
29 | //
30 | // @Summary Submit URL's for scanning
31 | // @Description Starts a new scanning routine for a list of URL's and options, writing results to the database.
32 | // @Tags Results
33 | // @Accept json
34 | // @Produce json
35 | // @Param query body submitRequest true "The URL scanning request object"
36 | // @Success 200 {string} string "Probing started"
37 | // @Router /submit [post]
38 | func (h *ApiHandler) SubmitHandler(w http.ResponseWriter, r *http.Request) {
39 | var request submitRequest
40 | if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
41 | log.Error("failed to read json request", "err", err)
42 | http.Error(w, "Error reading JSON request", http.StatusInternalServerError)
43 | return
44 | }
45 |
46 | if len(request.URLs) == 0 {
47 | http.Error(w, "No URLs provided", http.StatusBadRequest)
48 | return
49 | }
50 |
51 | options := runner.NewDefaultOptions()
52 | options.Scan.ScreenshotPath = h.ScreenshotPath
53 |
54 | // Override default values with request options
55 | if request.Options != nil {
56 | if request.Options.X != 0 {
57 | options.Chrome.WindowX = request.Options.X
58 | }
59 | if request.Options.Y != 0 {
60 | options.Chrome.WindowY = request.Options.Y
61 | }
62 | if request.Options.UserAgent != "" {
63 | options.Chrome.UserAgent = request.Options.UserAgent
64 | }
65 | if request.Options.Timeout != 0 {
66 | options.Scan.Timeout = request.Options.Timeout
67 | }
68 | if request.Options.Delay != 0 {
69 | options.Scan.Delay = request.Options.Delay
70 | }
71 | if request.Options.Format != "" {
72 | options.Scan.ScreenshotFormat = request.Options.Format
73 | }
74 | }
75 |
76 | writer, err := writers.NewDbWriter(h.DbURI, false)
77 | if err != nil {
78 | http.Error(w, "Error connecting to DB for writer", http.StatusInternalServerError)
79 | return
80 | }
81 |
82 | logger := slog.New(log.Logger)
83 |
84 | driver, err := driver.NewChromedp(logger, *options)
85 | if err != nil {
86 | http.Error(w, "Error sarting driver", http.StatusInternalServerError)
87 | return
88 | }
89 |
90 | runner, err := runner.NewRunner(logger, driver, *options, []writers.Writer{writer})
91 | if err != nil {
92 | log.Error("error starting runner", "err", err)
93 | http.Error(w, "Error starting runner", http.StatusInternalServerError)
94 | return
95 | }
96 |
97 | // have everything we need! start ther runner goroutine
98 | go dispatchRunner(runner, request.URLs)
99 |
100 | response := `Probing started`
101 | jsonData, err := json.Marshal(response)
102 | if err != nil {
103 | http.Error(w, "Error creating JSON response", http.StatusInternalServerError)
104 | return
105 | }
106 |
107 | w.Write(jsonData)
108 | }
109 |
110 | // dispatchRunner run's a runner in a separate goroutine
111 | func dispatchRunner(runner *runner.Runner, targets []string) {
112 | // feed in targets
113 | go func() {
114 | for _, url := range targets {
115 | runner.Targets <- url
116 | }
117 | close(runner.Targets)
118 | }()
119 |
120 | runner.Run()
121 | runner.Close()
122 | }
123 |
--------------------------------------------------------------------------------
/web/ui/src/lib/api/types.ts:
--------------------------------------------------------------------------------
1 | // stats
2 | type statistics = {
3 | dbsize: number;
4 | results: number;
5 | headers: number;
6 | consolelogs: number;
7 | networklogs: number;
8 | response_code_stats: response_code_stats[];
9 | };
10 |
11 | interface response_code_stats {
12 | code: number;
13 | count: number;
14 | }
15 |
16 | // wappalyzer
17 | type wappalyzer = {
18 | [name: string]: string;
19 | };
20 |
21 | // gallery
22 | type gallery = {
23 | results: galleryResult[];
24 | page: number;
25 | limit: number;
26 | total_count: number;
27 | };
28 |
29 | type galleryResult = {
30 | id: number;
31 | url: string;
32 | probed_at: string;
33 | title: string;
34 | response_code: number;
35 | file_name: string;
36 | screenshot: string;
37 | failed: boolean;
38 | technologies: string[];
39 | };
40 |
41 | // list
42 | type list = {
43 | id: number;
44 | url: string;
45 | final_url: string;
46 | response_code: number;
47 | response_reason: string;
48 | protocol: string;
49 | content_length: number;
50 | title: string;
51 | failed: boolean;
52 | failed_reason: string;
53 | };
54 |
55 | // details
56 | interface tls {
57 | id: number;
58 | result_id: number;
59 | protocol: string;
60 | key_exchange: string;
61 | cipher: string;
62 | subject_name: string;
63 | san_list: sanlist[];
64 | issuer: string;
65 | valid_from: string;
66 | valid_to: string;
67 | server_signature_algorithm: number;
68 | encrypted_client_hello: boolean;
69 | }
70 |
71 | interface sanlist {
72 | id: number;
73 | tls_id: number;
74 | value: string;
75 | }
76 |
77 | interface technology {
78 | id: number;
79 | result_id: number;
80 | value: string;
81 | }
82 |
83 | interface header {
84 | id: number;
85 | result_id: number;
86 | key: string;
87 | value: string | null;
88 | }
89 |
90 | interface networklog {
91 | id: number;
92 | result_id: number;
93 | request_type: number;
94 | status_code: number;
95 | url: string;
96 | remote_ip: string;
97 | mime_type: string;
98 | time: string;
99 | error: string;
100 | content: string;
101 | }
102 |
103 | interface consolelog {
104 | id: number;
105 | resultid: number;
106 | type: string;
107 | value: string;
108 | }
109 |
110 | interface cookie {
111 | id: number;
112 | result_id: number;
113 | name: string;
114 | value: string;
115 | domain: string;
116 | path: string;
117 | expires: string; // actually a timestamp
118 | size: number;
119 | http_only: boolean;
120 | secure: boolean;
121 | session: boolean;
122 | priority: string;
123 | source_scheme: string;
124 | source_port: number;
125 | }
126 |
127 | interface detail {
128 | id: number;
129 | url: string;
130 | probed_at: string;
131 | final_url: string;
132 | response_code: number;
133 | response_reason: string;
134 | protocol: string;
135 | content_length: number;
136 | html: string;
137 | title: string;
138 | perception_hash: string;
139 | file_name: string;
140 | is_pdf: boolean;
141 | failed: boolean;
142 | failed_reason: string;
143 | screenshot: string;
144 | tls: tls;
145 | technologies: technology[];
146 | headers: header[];
147 | network: networklog[];
148 | console: consolelog[];
149 | cookies: cookie[];
150 | }
151 |
152 | interface searchresult {
153 | id: number;
154 | url: string;
155 | final_url: string;
156 | response_code: number;
157 | content_length: number;
158 | title: string;
159 | matched_fields: string[];
160 | file_name: string;
161 | screenshot: string;
162 | }
163 |
164 | interface technologylist {
165 | technologies: string[];
166 | }
167 |
168 | export type {
169 | statistics,
170 | wappalyzer,
171 | gallery,
172 | list,
173 | galleryResult,
174 | tls,
175 | sanlist,
176 | technology,
177 | header,
178 | networklog,
179 | consolelog,
180 | cookie,
181 | detail,
182 | searchresult,
183 | technologylist,
184 | };
--------------------------------------------------------------------------------
/pkg/database/db.go:
--------------------------------------------------------------------------------
1 | package database
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net/url"
7 | "os"
8 | "path/filepath"
9 | "strings"
10 |
11 | "github.com/glebarez/sqlite"
12 | "github.com/sensepost/gowitness/pkg/models"
13 | "gorm.io/driver/mysql"
14 | "gorm.io/driver/postgres"
15 | "gorm.io/gorm"
16 | "gorm.io/gorm/logger"
17 | )
18 |
19 | // Connection returns a Database connection based on a URI
20 | func Connection(uri string, shouldExist, debug bool) (*gorm.DB, error) {
21 | var err error
22 | var c *gorm.DB
23 |
24 | db, err := url.Parse(uri)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | var config = &gorm.Config{}
30 | if debug {
31 | config.Logger = logger.Default.LogMode(logger.Info)
32 | } else {
33 | config.Logger = logger.Default.LogMode(logger.Error)
34 | }
35 |
36 | switch db.Scheme {
37 | case "sqlite":
38 | if shouldExist {
39 | dbpath := filepath.Join(db.Host, db.Path)
40 | dbpath = filepath.Clean(dbpath)
41 |
42 | if _, err := os.Stat(dbpath); os.IsNotExist(err) {
43 | return nil, fmt.Errorf("sqlite database file does not exist: %s", dbpath)
44 | } else if err != nil {
45 | return nil, fmt.Errorf("error checking sqlite database file: %w", err)
46 | }
47 | }
48 |
49 | c, err = gorm.Open(sqlite.Open(db.Host+db.Path+"?cache=shared"), config)
50 | if err != nil {
51 | return nil, err
52 | }
53 | c.Exec("PRAGMA foreign_keys = ON")
54 | case "postgres":
55 | dsn, err := convertPostgresURItoDSN(uri)
56 | if err != nil {
57 | return nil, err
58 | }
59 | c, err = gorm.Open(postgres.Open(dsn), config)
60 | if err != nil {
61 | return nil, err
62 | }
63 | case "mysql":
64 | dsn, err := convertMySQLURItoDSN(uri)
65 | if err != nil {
66 | return nil, err
67 | }
68 | c, err = gorm.Open(mysql.Open(dsn), config)
69 | if err != nil {
70 | return nil, err
71 | }
72 | default:
73 | return nil, errors.New("invalid db uri scheme")
74 | }
75 |
76 | // run database migrations on the connection
77 | if err := c.AutoMigrate(
78 | &models.Result{},
79 | &models.TLS{},
80 | &models.TLSSanList{},
81 | &models.Technology{},
82 | &models.Header{},
83 | &models.NetworkLog{},
84 | &models.ConsoleLog{},
85 | &models.Cookie{},
86 | ); err != nil {
87 | return nil, err
88 | }
89 |
90 | return c, nil
91 | }
92 |
93 | func convertMySQLURItoDSN(uri string) (string, error) {
94 | parsed, err := url.Parse(uri)
95 | if err != nil {
96 | return "", err
97 | }
98 |
99 | user := parsed.User.Username()
100 | pass, _ := parsed.User.Password()
101 | host := parsed.Host
102 | dbname := strings.TrimPrefix(parsed.Path, "/")
103 |
104 | // Handle "tcp(...)"
105 | if strings.HasPrefix(host, "tcp(") && strings.HasSuffix(host, ")") {
106 | host = strings.TrimPrefix(host, "tcp(")
107 | host = strings.TrimSuffix(host, ")")
108 | }
109 |
110 | // Default port
111 | if !strings.Contains(host, ":") {
112 | host = host + ":3306"
113 | }
114 |
115 | dsn := fmt.Sprintf(
116 | "%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
117 | user, pass, host, dbname,
118 | )
119 |
120 | return dsn, nil
121 | }
122 |
123 | func convertPostgresURItoDSN(uri string) (string, error) {
124 | parsed, err := url.Parse(uri)
125 | if err != nil {
126 | return "", err
127 | }
128 |
129 | user := parsed.User.Username()
130 | pass, _ := parsed.User.Password()
131 | host := parsed.Hostname()
132 | port := parsed.Port()
133 | if port == "" {
134 | port = "5432"
135 | }
136 |
137 | dbname := strings.TrimPrefix(parsed.Path, "/")
138 |
139 | // Start building the DSN
140 | dsn := fmt.Sprintf(
141 | "host=%s user=%s password=%s dbname=%s port=%s",
142 | host, user, pass, dbname, port,
143 | )
144 |
145 | // Add query params from URI
146 | query := parsed.Query()
147 | for key, values := range query {
148 | // Only take the first value per key
149 | dsn += fmt.Sprintf(" %s=%s", key, values[0])
150 | }
151 |
152 | return dsn, nil
153 | }
154 |
--------------------------------------------------------------------------------
/pkg/readers/nmap.go:
--------------------------------------------------------------------------------
1 | package readers
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 |
8 | "github.com/lair-framework/go-nmap"
9 | "github.com/sensepost/gowitness/internal/islazy"
10 | )
11 |
12 | // NmapReader is an Nmap results reader
13 | type NmapReader struct {
14 | Options *NmapReaderOptions
15 | }
16 |
17 | // NmapReaderOptions are options for the nmap reader
18 | type NmapReaderOptions struct {
19 | // Path to an Nmap XML file
20 | Source string
21 | NoHTTP bool
22 | NoHTTPS bool
23 | // OpenOnly will only scan ports marked as open
24 | OpenOnly bool
25 | // Ports to limit scans to
26 | Ports []int
27 | // Ports to exclude, no matter what
28 | ExcludePorts []int
29 | // SkipPorts are ports to not scan
30 | SkipPorts []int
31 | // ServiceContains is a partial service filter
32 | ServiceContains string
33 | // Services is a service limit
34 | Services []string
35 | // Hostname is a hostname to use for url targets
36 | Hostnames bool
37 | }
38 |
39 | // NewNmapReader prepares a new Nmap reader
40 | func NewNmapReader(opts *NmapReaderOptions) *NmapReader {
41 | return &NmapReader{
42 | Options: opts,
43 | }
44 | }
45 |
46 | // Read an nmap file
47 | func (nr *NmapReader) Read(ch chan<- string) error {
48 | defer close(ch)
49 |
50 | xml, err := os.ReadFile(nr.Options.Source)
51 | if err != nil {
52 | return err
53 | }
54 |
55 | nmapXML, err := nmap.Parse(xml)
56 | if err != nil {
57 | return err
58 | }
59 |
60 | for _, host := range nmapXML.Hosts {
61 | for _, address := range host.Addresses {
62 | if !islazy.SliceHasStr([]string{"ipv4", "ipv6"}, address.AddrType) {
63 | continue
64 | }
65 |
66 | for _, port := range host.Ports {
67 | // filter only open ports
68 | if nr.Options.OpenOnly && port.State.State != "open" {
69 | continue
70 | }
71 |
72 | // if this port should always be excluded
73 | if len(nr.Options.ExcludePorts) > 0 && !islazy.SliceHasInt(nr.Options.ExcludePorts, port.PortId) {
74 | continue
75 | }
76 |
77 | // apply the port filter if it exists
78 | if len(nr.Options.Ports) > 0 && !islazy.SliceHasInt(nr.Options.Ports, port.PortId) {
79 | continue
80 | }
81 |
82 | // apply port skips
83 | if len(nr.Options.SkipPorts) > 0 && islazy.SliceHasInt(nr.Options.SkipPorts, port.PortId) {
84 | continue
85 | }
86 |
87 | // apply service filter
88 | if len(nr.Options.Services) > 0 && !islazy.SliceHasStr(nr.Options.Services, port.Service.Name) {
89 | continue
90 | }
91 |
92 | // apply partial service filter
93 | if len(nr.Options.ServiceContains) > 0 && !strings.Contains(nr.Options.ServiceContains, port.Service.Name) {
94 | continue
95 | }
96 |
97 | // filters are complete, generate urls to push into the channel
98 |
99 | // add hostname candidates
100 | if nr.Options.Hostnames {
101 | for _, hostaName := range host.Hostnames {
102 | for _, target := range nr.urlsFor(hostaName.Name, port.PortId) {
103 | ch <- target
104 | }
105 | }
106 | }
107 |
108 | // ip:port candidates
109 | if address.AddrType == "ipv4" {
110 | for _, target := range nr.urlsFor(address.Addr, port.PortId) {
111 | ch <- target
112 | }
113 | } else {
114 | addr := fmt.Sprintf("[%s]", address.Addr)
115 | for _, target := range nr.urlsFor(addr, port.PortId) {
116 | ch <- target
117 | }
118 | }
119 | }
120 | }
121 | }
122 |
123 | return nil
124 | }
125 |
126 | // urlsFor returns URLs for a scanning candidate.
127 | // For candidates with no protocol, (and none of http/https is ignored), the
128 | // method will return two urls
129 | func (nr *NmapReader) urlsFor(target string, port int) []string {
130 | var urls []string
131 |
132 | if !nr.Options.NoHTTP {
133 | urls = append(urls, fmt.Sprintf("http://%s:%d", target, port))
134 | }
135 |
136 | if !nr.Options.NoHTTPS {
137 | urls = append(urls, fmt.Sprintf("https://%s:%d", target, port))
138 | }
139 |
140 | return urls
141 | }
142 |
--------------------------------------------------------------------------------
/web/ui/src/pages/dashboard/Dashboard.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { WideSkeleton } from "@/components/loading";
3 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
4 | import { DatabaseIcon, FileTextIcon, HardDriveIcon, NetworkIcon, TerminalIcon } from "lucide-react";
5 | import { Bar, BarChart, CartesianGrid, XAxis, YAxis, ResponsiveContainer } from "recharts";
6 | import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ui/chart";
7 | import * as apitypes from "@/lib/api/types";
8 | import { getData } from "./data";
9 |
10 | const chartConfig = {
11 | count: {
12 | label: "Total",
13 | color: "hsl(var(--chart-5))",
14 | },
15 | code: {
16 | label: "HTTP Status Code",
17 | color: "hsl(var(--chart-1))",
18 | },
19 | } satisfies ChartConfig;
20 |
21 | const StatCard = ({ title, value, icon: Icon }: { title: string; value: number | string; icon: React.ElementType; }) => (
22 |
23 |
24 | {title}
25 |
26 |
27 |
28 | {value}
29 |
30 |
31 | );
32 |
33 | export default function DashboardPage() {
34 | const [stats, setStats] = useState();
35 | const [loading, setLoading] = useState(true);
36 |
37 | useEffect(() => {
38 | getData(setLoading, setStats);
39 | }, []);
40 |
41 | if (loading) return ;
42 |
43 | return (
44 |
45 | Dashboard
46 |
47 |
52 |
57 |
62 |
67 |
72 |
73 |
74 |
75 | HTTP Status Code Distribution
76 |
77 |
78 |
79 |
80 |
81 |
82 |
87 | `${value}%`}
91 | />
92 | } />
93 | } />
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | );
102 | }
--------------------------------------------------------------------------------
/pkg/runner/options.go:
--------------------------------------------------------------------------------
1 | package runner
2 |
3 | // Options are global gowitness options
4 | type Options struct {
5 | // Logging is logging options
6 | Logging Logging
7 | // Chrome is Chrome related options
8 | Chrome Chrome
9 | // Writer is output options
10 | Writer Writer
11 | // Scan is typically Scan options
12 | Scan Scan
13 | }
14 |
15 | // Logging is log related options
16 | type Logging struct {
17 | // Debug display debug level logging
18 | Debug bool
19 | // LogScanErrors log errors related to scanning
20 | LogScanErrors bool
21 | // Silence all logging
22 | Silence bool
23 | }
24 |
25 | // Chrome is Google Chrome related options
26 | type Chrome struct {
27 | // Path to the Chrome binary. An empty value implies that
28 | // go-rod will auto download a platform appropriate binary
29 | // to use.
30 | Path string
31 | // WSS is a websocket URL. Setting this will prevent gowitness
32 | // form launching Chrome, but rather use the remote instance.
33 | WSS string
34 | // Proxy server to use
35 | Proxy string
36 | // UserAgent is the user-agent string to set for Chrome
37 | UserAgent string
38 | // Headers to add to every request
39 | Headers []string
40 | // WindowSize, in pixels. Eg; X=1920,Y=1080
41 | WindowX int
42 | WindowY int
43 | }
44 |
45 | // Writer options
46 | type Writer struct {
47 | Db bool
48 | DbURI string
49 | DbDebug bool // enables verbose database logs
50 | Csv bool
51 | CsvFile string
52 | Jsonl bool
53 | JsonlFile string
54 | Stdout bool
55 | None bool
56 | }
57 |
58 | // Scan is scanning related options
59 | type Scan struct {
60 | // The scan driver to use. Can be one of [gorod, chromedp]
61 | Driver string
62 | // Threads (not really) are the number of goroutines to use.
63 | // More soecifically, its the go-rod page pool well use.
64 | Threads int
65 | // Timeout is the maximum time to wait for a page load before timing out.
66 | Timeout int
67 | // Number of seconds of delay between navigation and screenshotting
68 | Delay int
69 | // UriFilter are URI's that are okay to process. This should normally
70 | // be http and https
71 | UriFilter []string
72 | // Don't write HTML response content
73 | SkipHTML bool
74 | // SkipNetworkLogs stops recording individual request/response entries
75 | SkipNetworkLogs bool
76 | // ScreenshotPath is the path where screenshot images will be stored.
77 | // An empty value means drivers will not write screenshots to disk. In
78 | // that case, you'd need to specify writer saves.
79 | ScreenshotPath string
80 | // ScreenshotFormat to save as
81 | ScreenshotFormat string
82 | // ScreenshotJpegQuality is the quality of the JPEG screenshot (1-100)
83 | ScreenshotJpegQuality int
84 | // ScreenshotFullPage saves full, scrolled web pages
85 | ScreenshotFullPage bool
86 | // ScreenshotToWriter passes screenshots as a model property to writers
87 | ScreenshotToWriter bool
88 | // ScreenshotSkipSave skips saving screenshots to disk
89 | ScreenshotSkipSave bool
90 | // JavaScript to evaluate on every page
91 | JavaScript string
92 | JavaScriptFile string
93 | // Save content stores content from network requests (warning) this
94 | // could make written artefacts huge
95 | SaveContent bool
96 | // HttpCodeFilter are http response codes to screenshot. this is a filter.
97 | // by default all codes are screenshotted
98 | HttpCodeFilter []int
99 | }
100 |
101 | // NewDefaultOptions returns Options with some default values
102 | func NewDefaultOptions() *Options {
103 | return &Options{
104 | Chrome: Chrome{
105 | UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
106 | WindowX: 1920,
107 | WindowY: 1080,
108 | },
109 | Scan: Scan{
110 | Driver: "chromedp",
111 | Threads: 6,
112 | Timeout: 60,
113 | UriFilter: []string{"http", "https"},
114 | ScreenshotFormat: "jpeg",
115 | HttpCodeFilter: []int{},
116 | },
117 | Logging: Logging{
118 | Debug: true,
119 | LogScanErrors: true,
120 | },
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/pkg/models/oldv2/models.go:
--------------------------------------------------------------------------------
1 | package oldv2
2 |
3 | import (
4 | "encoding/json"
5 | "strconv"
6 | "time"
7 |
8 | "gorm.io/gorm"
9 | )
10 |
11 | // URL contains information about a URL
12 | type URL struct {
13 | gorm.Model
14 |
15 | URL string
16 | FinalURL string
17 | ResponseCode int
18 | ResponseReason string
19 | Proto string
20 | ContentLength int64
21 | Title string
22 | Filename string
23 | IsPDF bool
24 | PerceptionHash string
25 | DOM string
26 | Screenshot string
27 |
28 | TLS TLS
29 |
30 | Headers []Header
31 | Technologies []Technologie
32 | Console []ConsoleLog
33 | Network []NetworkLog
34 | }
35 |
36 | // AddHeader adds a new header to a URL
37 | func (url *URL) AddHeader(key string, value string) {
38 | url.Headers = append(url.Headers, Header{
39 | Key: key,
40 | Value: value,
41 | })
42 | }
43 |
44 | // AddTechnlogies adds a new technologies to a URL
45 | func (url *URL) AddTechnologie(value string) {
46 | url.Technologies = append(url.Technologies, Technologie{
47 | Value: value,
48 | })
49 | }
50 |
51 | // MarshallCSV returns values as a slice
52 | func (url *URL) MarshallCSV() (res []string) {
53 | return []string{url.URL,
54 | url.FinalURL,
55 | strconv.Itoa(url.ResponseCode),
56 | url.ResponseReason,
57 | url.Proto,
58 | strconv.Itoa(int(url.ContentLength)),
59 | url.Title,
60 | url.Filename}
61 | }
62 |
63 | // MarshalJSON returns JSON encoding of url. Implements json.Marshaler.
64 | func (url *URL) MarshalJSON() ([]byte, error) {
65 | var tmp struct {
66 | URL string
67 | FinalURL string
68 | ResponseCode int
69 | ResponseReason string
70 | Proto string
71 | ContentLength int64
72 | Title string
73 | Filename string
74 | }
75 |
76 | tmp.URL = url.URL
77 | tmp.FinalURL = url.FinalURL
78 | tmp.ResponseCode = url.ResponseCode
79 | tmp.ResponseReason = url.ResponseReason
80 | tmp.Proto = url.Proto
81 | tmp.ContentLength = url.ContentLength
82 | tmp.Title = url.Title
83 | tmp.Filename = url.Filename
84 |
85 | return json.Marshal(&tmp)
86 | }
87 |
88 | // Header contains an HTTP header
89 | type Header struct {
90 | gorm.Model
91 |
92 | URLID uint
93 |
94 | Key string
95 | Value string
96 | }
97 |
98 | // Technologie contains a technologie
99 | type Technologie struct {
100 | gorm.Model
101 |
102 | URLID uint
103 |
104 | Value string
105 | }
106 |
107 | // TLS contains TLS information for a URL
108 | type TLS struct {
109 | gorm.Model
110 |
111 | URLID uint
112 |
113 | Version uint16
114 | ServerName string
115 | TLSCertificates []TLSCertificate
116 | }
117 |
118 | // TLSCertificate contain TLS Certificate information
119 | type TLSCertificate struct {
120 | gorm.Model
121 |
122 | TLSID uint
123 |
124 | Raw []byte
125 | DNSNames []TLSCertificateDNSName
126 | SubjectCommonName string
127 | IssuerCommonName string
128 | SignatureAlgorithm string
129 | PubkeyAlgorithm string
130 | }
131 |
132 | // AddDNSName adds a new DNS Name to a Certificate
133 | func (tlsCert *TLSCertificate) AddDNSName(name string) {
134 | tlsCert.DNSNames = append(tlsCert.DNSNames, TLSCertificateDNSName{Name: name})
135 | }
136 |
137 | // TLSCertificateDNSName has DNS names for a TLS certificate
138 | type TLSCertificateDNSName struct {
139 | gorm.Model
140 |
141 | TLSCertificateID uint
142 | Name string
143 | }
144 |
145 | // ConsoleLog contains the console log, and exceptions emitted
146 | type ConsoleLog struct {
147 | gorm.Model
148 |
149 | URLID uint
150 |
151 | Time time.Time
152 | Type string
153 | Value string
154 | }
155 |
156 | // RequestType are network log types
157 | type RequestType int
158 |
159 | const (
160 | HTTP RequestType = 0
161 | WS
162 | )
163 |
164 | // NetworkLog contains Chrome networks events that were emitted
165 | type NetworkLog struct {
166 | gorm.Model
167 |
168 | URLID uint
169 |
170 | RequestID string
171 | RequestType RequestType
172 | StatusCode int64
173 | URL string
174 | FinalURL string
175 | IP string
176 | Time time.Time
177 | Error string
178 | }
179 |
--------------------------------------------------------------------------------
/cmd/scan_nmap.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 |
6 | "github.com/sensepost/gowitness/internal/ascii"
7 | "github.com/sensepost/gowitness/internal/islazy"
8 | "github.com/sensepost/gowitness/pkg/log"
9 | "github.com/sensepost/gowitness/pkg/readers"
10 | "github.com/spf13/cobra"
11 | )
12 |
13 | var nmapCmdOptions = &readers.NmapReaderOptions{}
14 | var nmapCmd = &cobra.Command{
15 | Use: "nmap",
16 | Short: "Scan targets from an Nmap XML file",
17 | Long: ascii.LogoHelp(ascii.Markdown(`
18 | # scan nmap
19 |
20 | Scan targets from an Nmap XML file.
21 |
22 | When performing Nmap scans, specify the -oX nmap.xml flag to store data in an
23 | XML-formatted file that gowitness can parse.
24 |
25 | By default, this command will try and screenshot all ports specified in an
26 | nmap.xml results file. That means it will try and do silly things like
27 | screenshot SSH services, which obviously won't work. It's for this reason that
28 | you'd want to specify the ports or services to parse using the --port and
29 | --service / --service-contains flags. For most HTTP-based services, try:
30 | - --service http
31 | - --service http-alt
32 | - --service http-mgmt
33 | - --service http-proxy
34 | - --service https
35 | - --service https-alt
36 |
37 | On ports, when specifying --port (can be multiple), target candidates will only
38 | be generated for results that match one of the specified ports. In contrast,
39 | when --exclude-port (can also be multiple) is set, no candidates for that port
40 | will be generated.
41 |
42 | **Note**: By default, no metadata is saved except for screenshots that are
43 | stored in the configured --screenshot-path. For later parsing (i.e., using the
44 | gowitness reporting feature), you need to specify where to write results (db,
45 | csv, jsonl) using the _--write-*_ set of flags. See _--help_ for available
46 | flags.`)),
47 | Example: ascii.Markdown(`
48 | - gowitness scan nmap -f ~/Desktop/targets.xml --write-json --write-db
49 | - gowitness scan nmap -f targets.xml --threads 50 --no-http --service https
50 | - gowitness scan nmap -f /tmp/n.xml --open-only --port 80 --port 443 --port 8080
51 | - gowitness scan nmap -f ~/nmap.xml --open-only --service-contains http`),
52 | PreRunE: func(cmd *cobra.Command, args []string) error {
53 | if nmapCmdOptions.Source == "" {
54 | return errors.New("a source must be specified")
55 | }
56 |
57 | if !islazy.FileExists(nmapCmdOptions.Source) {
58 | return errors.New("source is not readable")
59 | }
60 |
61 | return nil
62 | },
63 | Run: func(cmd *cobra.Command, args []string) {
64 | log.Debug("starting Nmap file scanning", "file", nmapCmdOptions.Source)
65 |
66 | reader := readers.NewNmapReader(nmapCmdOptions)
67 | go func() {
68 | if err := reader.Read(scanRunner.Targets); err != nil {
69 | log.Error("error in reader.Read", "err", err)
70 | return
71 | }
72 | }()
73 |
74 | scanRunner.Run()
75 | scanRunner.Close()
76 | },
77 | }
78 |
79 | func init() {
80 | scanCmd.AddCommand(nmapCmd)
81 |
82 | nmapCmd.Flags().StringVarP(&nmapCmdOptions.Source, "file", "f", "", "A file with targets to scan. Use - for stdin")
83 | nmapCmd.Flags().BoolVar(&nmapCmdOptions.NoHTTP, "no-http", false, "Do not add 'http://' to targets where missing")
84 | nmapCmd.Flags().BoolVar(&nmapCmdOptions.NoHTTPS, "no-https", false, "Do not add 'https://' to targets where missing")
85 | nmapCmd.Flags().BoolVarP(&nmapCmdOptions.OpenOnly, "open-only", "o", false, "Only scan ports marked as open")
86 | nmapCmd.Flags().IntSliceVar(&nmapCmdOptions.Ports, "port", []int{}, "A port filter to apply. Supports multiple --port flags")
87 | nmapCmd.Flags().IntSliceVar(&nmapCmdOptions.ExcludePorts, "exclude-port", []int{}, "A port exclusion filter to apply. Supports multiple --exclude-port flags")
88 | nmapCmd.Flags().IntSliceVar(&nmapCmdOptions.SkipPorts, "skip-port", []int{}, "Do not scan these ports. Supports multiple --skip-port flags")
89 | nmapCmd.Flags().StringVar(&nmapCmdOptions.ServiceContains, "service-contains", "", "A service name filter. Will check if service 'contains' this value first")
90 | nmapCmd.Flags().StringSliceVar(&nmapCmdOptions.Services, "service", []string{}, "A service filter to apply. Supports multiple --service flags")
91 | nmapCmd.Flags().BoolVar(&nmapCmdOptions.Hostnames, "hostnames", false, "Add hostnames in URL candidates (useful for virtual hosting)")
92 | }
93 |
--------------------------------------------------------------------------------
/web/ui/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DialogPrimitive from "@radix-ui/react-dialog"
3 | import { Cross2Icon } from "@radix-ui/react-icons"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Dialog = DialogPrimitive.Root
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger
10 |
11 | const DialogPortal = DialogPrimitive.Portal
12 |
13 | const DialogClose = DialogPrimitive.Close
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, children, ...props }, ref) => (
34 |
35 |
36 |
44 | {children}
45 |
46 |
47 | Close
48 |
49 |
50 |
51 | ))
52 | DialogContent.displayName = DialogPrimitive.Content.displayName
53 |
54 | const DialogHeader = ({
55 | className,
56 | ...props
57 | }: React.HTMLAttributes) => (
58 |
65 | )
66 | DialogHeader.displayName = "DialogHeader"
67 |
68 | const DialogFooter = ({
69 | className,
70 | ...props
71 | }: React.HTMLAttributes) => (
72 |
79 | )
80 | DialogFooter.displayName = "DialogFooter"
81 |
82 | const DialogTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
94 | ))
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogTrigger,
114 | DialogClose,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription,
120 | }
121 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "path/filepath"
7 | "runtime"
8 | "runtime/pprof"
9 | "runtime/trace"
10 | "time"
11 |
12 | "github.com/sensepost/gowitness/internal/ascii"
13 | "github.com/sensepost/gowitness/pkg/log"
14 | "github.com/sensepost/gowitness/pkg/runner"
15 | "github.com/spf13/cobra"
16 | )
17 |
18 | var (
19 | opts = &runner.Options{}
20 |
21 | // perf profiling
22 | enableProfiling bool
23 | profileDir string
24 |
25 | // hooks to run after command execution
26 | postRunHooks []func()
27 | )
28 |
29 | var rootCmd = &cobra.Command{
30 | Use: "gowitness",
31 | Short: "A web screenshot and information gathering tool",
32 | Long: ascii.Logo(),
33 | PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
34 | if opts.Logging.Silence {
35 | log.EnableSilence()
36 | }
37 |
38 | if opts.Logging.Debug && !opts.Logging.Silence {
39 | // also enable logging of scan errors. often this is _actually_ what
40 | // people want when they enable debug logging.
41 | opts.Logging.LogScanErrors = true
42 | log.EnableDebug()
43 | log.Debug("debug logging enabled. this also enabled --log-scan-errors")
44 | }
45 |
46 | if enableProfiling {
47 | ts := time.Now().Format("20060102-150405")
48 | profileDir = filepath.Join("profiles", ts)
49 |
50 | if err := os.MkdirAll(profileDir, 0o755); err != nil {
51 | return fmt.Errorf("could not create profile directory %q: %w", profileDir, err)
52 | }
53 |
54 | cpuPath := filepath.Join(profileDir, "cpu.pprof")
55 | memPath := filepath.Join(profileDir, "mem.pprof")
56 | tracePath := filepath.Join(profileDir, "trace.out")
57 |
58 | // cpu
59 | cpuFile, err := os.Create(cpuPath)
60 | if err != nil {
61 | return fmt.Errorf("could not create CPU profile file: %w", err)
62 | }
63 | if err := pprof.StartCPUProfile(cpuFile); err != nil {
64 | _ = cpuFile.Close()
65 | return fmt.Errorf("could not start CPU profile: %w", err)
66 | }
67 | postRunHooks = append(postRunHooks, func() {
68 | pprof.StopCPUProfile()
69 | _ = cpuFile.Close()
70 | })
71 |
72 | // memory
73 | postRunHooks = append(postRunHooks, func() {
74 | memFile, err := os.Create(memPath)
75 | if err != nil {
76 | fmt.Fprintf(os.Stderr, "could not create memory profile file: %v\n", err)
77 | return
78 | }
79 | defer memFile.Close()
80 |
81 | runtime.GC() // refresh heap statistics
82 |
83 | if err := pprof.WriteHeapProfile(memFile); err != nil {
84 | fmt.Fprintf(os.Stderr, "could not write memory profile: %v\n", err)
85 | }
86 | })
87 |
88 | // trace
89 | traceFile, err := os.Create(tracePath)
90 | if err != nil {
91 | return fmt.Errorf("could not create trace file: %w", err)
92 | }
93 | if err := trace.Start(traceFile); err != nil {
94 | _ = traceFile.Close()
95 | return fmt.Errorf("could not start trace: %w", err)
96 | }
97 | postRunHooks = append(postRunHooks, func() {
98 | trace.Stop()
99 | _ = traceFile.Close()
100 | })
101 |
102 | // Log where results will be written
103 | log.Info(fmt.Sprintf("profiling enabled: writing profiles to %s", profileDir))
104 | }
105 |
106 | return nil
107 | },
108 |
109 | PersistentPostRun: func(cmd *cobra.Command, args []string) {
110 | for _, hook := range postRunHooks {
111 | hook()
112 | }
113 | },
114 | }
115 |
116 | func Execute() {
117 | rootCmd.CompletionOptions.DisableDefaultCmd = true
118 | rootCmd.SilenceErrors = true
119 | err := rootCmd.Execute()
120 | if err != nil {
121 | var cmd string
122 | c, _, cerr := rootCmd.Find(os.Args[1:])
123 | if cerr == nil {
124 | cmd = c.Name()
125 | }
126 |
127 | v := "\n"
128 |
129 | if cmd != "" {
130 | v += fmt.Sprintf("An error occured running the `%s` command\n", cmd)
131 | } else {
132 | v += "An error has occured. "
133 | }
134 |
135 | v += "The error was:\n\n" + fmt.Sprintf("```%s```", err)
136 | fmt.Println(ascii.Markdown(v))
137 |
138 | os.Exit(1)
139 | }
140 | }
141 |
142 | func init() {
143 | rootCmd.PersistentFlags().BoolVarP(&opts.Logging.Debug, "debug-log", "D", false, "Enable debug logging")
144 | rootCmd.PersistentFlags().BoolVarP(&opts.Logging.Silence, "quiet", "q", false, "Silence (almost all) logging")
145 | rootCmd.PersistentFlags().BoolVar(&enableProfiling, "profile", false, "Enable CPU, memory, and trace profiling (writes to profiles//)")
146 | }
147 |
--------------------------------------------------------------------------------
/pkg/readers/nessus.go:
--------------------------------------------------------------------------------
1 | package readers
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 | "net"
7 | "os"
8 |
9 | "github.com/sensepost/gowitness/internal/islazy"
10 | )
11 |
12 | // NessusReader is a Nessus file reader
13 | type NessusReader struct {
14 | Options *NessusReaderOptions
15 | }
16 |
17 | // NessusReaderOptions are options for a nessus file reader
18 | type NessusReaderOptions struct {
19 | Source string
20 | NoHTTP bool
21 | NoHTTPS bool
22 | Hostnames bool
23 | // filters
24 | Services []string
25 | PluginOutputs []string
26 | PluginNames []string
27 | Ports []int
28 | }
29 |
30 | // structures for XML parsing
31 | type reportHost struct {
32 | HostName string `xml:"name,attr"`
33 | ReportItems []reportItem `xml:"ReportItem"`
34 | Tags []tag `xml:"HostProperties>tag"`
35 | }
36 |
37 | type tag struct {
38 | Key string `xml:"name,attr"`
39 | Value string `xml:",chardata"`
40 | }
41 |
42 | type reportItem struct {
43 | PluginName string `xml:"pluginName,attr"`
44 | ServiceName string `xml:"svc_name,attr"`
45 | Port int `xml:"port,attr"`
46 | PluginOutput string `xml:"plugin_output"`
47 | }
48 |
49 | // NewNessusReader returns a new Nessus file reader
50 | func NewNessusReader(opts *NessusReaderOptions) *NessusReader {
51 | return &NessusReader{
52 | Options: opts,
53 | }
54 | }
55 |
56 | func (nr *NessusReader) Read(ch chan<- string) error {
57 | defer close(ch)
58 |
59 | nessus, err := os.Open(nr.Options.Source)
60 | if err != nil {
61 | return err
62 | }
63 | defer nessus.Close()
64 |
65 | decoder := xml.NewDecoder(nessus)
66 | var targets = make(map[string][]int)
67 |
68 | for {
69 | token, err := decoder.Token()
70 | if err != nil || token == nil {
71 | break // EOF or error
72 | }
73 |
74 | switch element := token.(type) {
75 | case xml.StartElement:
76 | if element.Name.Local != "ReportHost" {
77 | break
78 | }
79 |
80 | var host reportHost
81 | decoder.DecodeElement(&host, &element)
82 |
83 | var fqdn, ip string
84 | for _, v := range host.Tags {
85 | if v.Key == "host-fqdn" {
86 | fqdn = v.Value
87 | }
88 | if v.Key == "host-ip" {
89 | ip = v.Value
90 | }
91 | }
92 |
93 | for _, item := range host.ReportItems {
94 | // for future parsing debugging <3
95 | // log.Debug("report item", "ip", ip, "fqdn", fqdn)
96 | // log.Debug("detail", "plugin", item.PluginName, "service", item.ServiceName)
97 |
98 | // skip port if the port does not match the provided ports to filter
99 | if len(nr.Options.Ports) > 0 && !islazy.SliceHasInt(nr.Options.Ports, item.Port) {
100 | continue
101 | }
102 |
103 | // check the plugin name contains a given string. Contains should work, though startsWith may be useful.
104 | // A valid plugin name must be given here, otherwise we'll be iterating across too many pointless plugins.
105 | if !islazy.SliceHasStr(nr.Options.PluginNames, item.PluginName) {
106 | continue
107 | }
108 |
109 | // check the service name. typically this will at least be "web server" and or whatever plugin output
110 | if islazy.SliceHasStr(nr.Options.Services, item.ServiceName) ||
111 | islazy.SliceHasStr(nr.Options.PluginOutputs, item.PluginOutput) {
112 |
113 | // Add the hostnames or IP to the merged targetsMap
114 | if nr.Options.Hostnames && fqdn != "" {
115 | targets[fqdn] = islazy.UniqueIntSlice(append(targets[fqdn], item.Port))
116 | }
117 | if ip != "" {
118 | targets[ip] = islazy.UniqueIntSlice(append(targets[ip], item.Port))
119 | }
120 | }
121 | }
122 | }
123 | }
124 |
125 | for host, ports := range targets {
126 | for _, target := range nr.urlsFor(host, ports) {
127 | ch <- target
128 | }
129 | }
130 |
131 | return nil
132 | }
133 |
134 | // urlsFor generates urls for a target and its port ranges
135 | func (nr *NessusReader) urlsFor(target string, ports []int) []string {
136 | var urls []string
137 |
138 | ip := net.ParseIP(target)
139 |
140 | host := target
141 | if ip != nil && ip.To4() == nil {
142 | host = fmt.Sprintf("[%s]", target)
143 | }
144 |
145 | for _, port := range ports {
146 | if !nr.Options.NoHTTP {
147 | urls = append(urls, fmt.Sprintf("http://%s:%d", host, port))
148 | }
149 | if !nr.Options.NoHTTPS {
150 | urls = append(urls, fmt.Sprintf("https://%s:%d", host, port))
151 | }
152 | }
153 |
154 | return urls
155 | }
156 |
--------------------------------------------------------------------------------
/web/ui/src/lib/api/api.ts:
--------------------------------------------------------------------------------
1 | import { gallery, list, statistics, wappalyzer, detail, searchresult, technologylist } from "@/lib/api/types";
2 |
3 | const endpoints = {
4 | // api base path
5 | base: {
6 | path: import.meta.env.VITE_GOWITNESS_API_BASE_URL
7 | ? import.meta.env.VITE_GOWITNESS_API_BASE_URL + `/api`
8 | : `/api`,
9 | returnas: [] // n/a
10 | },
11 | // screenshot path
12 | screenshot: {
13 | path: import.meta.env.VITE_GOWITNESS_API_BASE_URL
14 | ? import.meta.env.VITE_GOWITNESS_API_BASE_URL + `/screenshots`
15 | : `/screenshots`,
16 | returnas: [] // n/a
17 | },
18 |
19 | // get endpoints
20 | statistics: {
21 | path: `/statistics`,
22 | returnas: {} as statistics
23 | },
24 | wappalyzer: {
25 | path: `/wappalyzer`,
26 | returnas: {} as wappalyzer
27 | },
28 | gallery: {
29 | path: `/results/gallery`,
30 | returnas: {} as gallery
31 | },
32 | list: {
33 | path: `/results/list`,
34 | returnas: [] as list[]
35 | },
36 | detail: {
37 | path: `/results/detail/:id`,
38 | returnas: {} as detail
39 | },
40 | technology: {
41 | path: `/results/technology`,
42 | returnas: {} as technologylist
43 | },
44 |
45 | // post endpoints
46 | search: {
47 | path: `/search`,
48 | returnas: {} as searchresult
49 | },
50 | delete: {
51 | path: `/results/delete`,
52 | returnas: "" as string
53 | },
54 | submit: {
55 | path: `/submit`,
56 | returnas: "" as string
57 | },
58 | submitsingle: {
59 | path: `/submit/single`,
60 | returnas: {} as detail
61 | }
62 | };
63 |
64 | type Endpoints = typeof endpoints;
65 | type EndpointReturnType = Endpoints[K]['returnas'];
66 |
67 | const replacePathParams = (path: string, params?: Record): [string, Record] => {
68 | if (!params) return [path, {}];
69 |
70 | const paramRegex = /:([a-zA-Z0-9_]+)/g;
71 | const missingParams: string[] = [];
72 | const remainingParams = { ...params }; // Create a copy of the params object to modify
73 |
74 | // Replace all `:param` placeholders with the corresponding values from params
75 | const newPath = path.replace(paramRegex, (match, paramName) => {
76 | if (paramName in remainingParams) {
77 | const value = remainingParams[paramName];
78 | delete remainingParams[paramName];
79 | return encodeURIComponent(value.toString());
80 | } else {
81 | missingParams.push(paramName);
82 | return match;
83 | }
84 | });
85 |
86 | // If any required params were missing, throw an error
87 | if (missingParams.length > 0) {
88 | throw new Error(`Missing required parameters: ${missingParams.join(', ')}`);
89 | }
90 |
91 | return [newPath, remainingParams];
92 | };
93 |
94 | const serializeParams = (params: Record) => {
95 | const query = new URLSearchParams();
96 | Object.entries(params).forEach(([key, value]) => {
97 | query.append(key, value.toString());
98 | });
99 | return query.toString() ? `?${query.toString()}` : '';
100 | };
101 |
102 | const get = async (
103 | endpointKey: K,
104 | params?: Record,
105 | raw: boolean = false
106 | ): Promise> => {
107 |
108 | const endpoint = endpoints[endpointKey];
109 | const [pathWithParams, remainingParams] = replacePathParams(endpoint.path, params);
110 | const queryString = remainingParams ? serializeParams(remainingParams) : '';
111 |
112 | const res = await fetch(`${endpoints.base.path}${pathWithParams}${queryString}`);
113 |
114 | if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
115 |
116 | if (raw) return await res.text() as unknown as EndpointReturnType;
117 | return await res.json() as EndpointReturnType;
118 | };
119 |
120 | const post = async (
121 | endpointKey: K,
122 | data?: unknown
123 | ): Promise> => {
124 |
125 | const endpoint = endpoints[endpointKey];
126 | const res = await fetch(`${endpoints.base.path}${endpoint.path}`, {
127 | method: 'POST',
128 | headers: { 'Content-Type': 'application/json' },
129 | body: JSON.stringify(data)
130 | });
131 |
132 | if (!res.ok) throw new Error(`HTTP Error: ${res.status}`);
133 |
134 | return await res.json() as EndpointReturnType;
135 | };
136 |
137 | export { endpoints, get, post };
--------------------------------------------------------------------------------
/web/ui/src/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import type {
4 | ToastActionElement,
5 | ToastProps,
6 | } from "@/components/ui/toast"
7 |
8 | const TOAST_LIMIT = 1
9 | const TOAST_REMOVE_DELAY = 1000000
10 |
11 | type ToasterToast = ToastProps & {
12 | id: string
13 | title?: React.ReactNode
14 | description?: React.ReactNode
15 | action?: ToastActionElement
16 | }
17 |
18 | const actionTypes = {
19 | ADD_TOAST: "ADD_TOAST",
20 | UPDATE_TOAST: "UPDATE_TOAST",
21 | DISMISS_TOAST: "DISMISS_TOAST",
22 | REMOVE_TOAST: "REMOVE_TOAST",
23 | } as const
24 |
25 | let count = 0
26 |
27 | function genId() {
28 | count = (count + 1) % Number.MAX_SAFE_INTEGER
29 | return count.toString()
30 | }
31 |
32 | type ActionType = typeof actionTypes
33 |
34 | type Action =
35 | | {
36 | type: ActionType["ADD_TOAST"]
37 | toast: ToasterToast
38 | }
39 | | {
40 | type: ActionType["UPDATE_TOAST"]
41 | toast: Partial
42 | }
43 | | {
44 | type: ActionType["DISMISS_TOAST"]
45 | toastId?: ToasterToast["id"]
46 | }
47 | | {
48 | type: ActionType["REMOVE_TOAST"]
49 | toastId?: ToasterToast["id"]
50 | }
51 |
52 | interface State {
53 | toasts: ToasterToast[]
54 | }
55 |
56 | const toastTimeouts = new Map>()
57 |
58 | const addToRemoveQueue = (toastId: string) => {
59 | if (toastTimeouts.has(toastId)) {
60 | return
61 | }
62 |
63 | const timeout = setTimeout(() => {
64 | toastTimeouts.delete(toastId)
65 | dispatch({
66 | type: "REMOVE_TOAST",
67 | toastId: toastId,
68 | })
69 | }, TOAST_REMOVE_DELAY)
70 |
71 | toastTimeouts.set(toastId, timeout)
72 | }
73 |
74 | export const reducer = (state: State, action: Action): State => {
75 | switch (action.type) {
76 | case "ADD_TOAST":
77 | return {
78 | ...state,
79 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
80 | }
81 |
82 | case "UPDATE_TOAST":
83 | return {
84 | ...state,
85 | toasts: state.toasts.map((t) =>
86 | t.id === action.toast.id ? { ...t, ...action.toast } : t
87 | ),
88 | }
89 |
90 | case "DISMISS_TOAST": {
91 | const { toastId } = action
92 |
93 | // ! Side effects ! - This could be extracted into a dismissToast() action,
94 | // but I'll keep it here for simplicity
95 | if (toastId) {
96 | addToRemoveQueue(toastId)
97 | } else {
98 | state.toasts.forEach((toast) => {
99 | addToRemoveQueue(toast.id)
100 | })
101 | }
102 |
103 | return {
104 | ...state,
105 | toasts: state.toasts.map((t) =>
106 | t.id === toastId || toastId === undefined
107 | ? {
108 | ...t,
109 | open: false,
110 | }
111 | : t
112 | ),
113 | }
114 | }
115 | case "REMOVE_TOAST":
116 | if (action.toastId === undefined) {
117 | return {
118 | ...state,
119 | toasts: [],
120 | }
121 | }
122 | return {
123 | ...state,
124 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
125 | }
126 | }
127 | }
128 |
129 | const listeners: Array<(state: State) => void> = []
130 |
131 | let memoryState: State = { toasts: [] }
132 |
133 | function dispatch(action: Action) {
134 | memoryState = reducer(memoryState, action)
135 | listeners.forEach((listener) => {
136 | listener(memoryState)
137 | })
138 | }
139 |
140 | type Toast = Omit
141 |
142 | function toast({ ...props }: Toast) {
143 | const id = genId()
144 |
145 | const update = (props: ToasterToast) =>
146 | dispatch({
147 | type: "UPDATE_TOAST",
148 | toast: { ...props, id },
149 | })
150 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
151 |
152 | dispatch({
153 | type: "ADD_TOAST",
154 | toast: {
155 | ...props,
156 | id,
157 | open: true,
158 | onOpenChange: (open) => {
159 | if (!open) dismiss()
160 | },
161 | },
162 | })
163 |
164 | return {
165 | id: id,
166 | dismiss,
167 | update,
168 | }
169 | }
170 |
171 | function useToast() {
172 | const [state, setState] = React.useState(memoryState)
173 |
174 | React.useEffect(() => {
175 | listeners.push(setState)
176 | return () => {
177 | const index = listeners.indexOf(setState)
178 | if (index > -1) {
179 | listeners.splice(index, 1)
180 | }
181 | }
182 | }, [state])
183 |
184 | return {
185 | ...state,
186 | toast,
187 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
188 | }
189 | }
190 |
191 | export { useToast, toast }
192 |
--------------------------------------------------------------------------------
/pkg/models/models.go:
--------------------------------------------------------------------------------
1 | package models
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | // RequestType are network log types
8 | type RequestType int
9 |
10 | const (
11 | HTTP RequestType = 0
12 | WS
13 | )
14 |
15 | // Result is a Gowitness result
16 | type Result struct {
17 | ID uint `json:"id" gorm:"primarykey"`
18 |
19 | URL string `json:"url"`
20 | ProbedAt time.Time `json:"probed_at"`
21 | FinalURL string `json:"final_url"`
22 | ResponseCode int `json:"response_code"`
23 | ResponseReason string `json:"response_reason"`
24 | Protocol string `json:"protocol"`
25 | ContentLength int64 `json:"content_length"`
26 | HTML string `json:"html" gorm:"type:longtext;index:,length:191"`
27 | Title string `json:"title" gorm:"index"`
28 | PerceptionHash string `json:"perception_hash" gorm:"index"`
29 | PerceptionHashGroupId uint `json:"perception_hash_group_id" gorm:"index"`
30 | Screenshot string `json:"screenshot"`
31 |
32 | // Name of the screenshot file
33 | Filename string `json:"file_name"`
34 | IsPDF bool `json:"is_pdf"`
35 |
36 | // Failed flag set if the result should be considered failed
37 | Failed bool `json:"failed"`
38 | FailedReason string `json:"failed_reason"`
39 |
40 | TLS TLS `json:"tls" gorm:"constraint:OnDelete:CASCADE"`
41 | Technologies []Technology `json:"technologies" gorm:"constraint:OnDelete:CASCADE"`
42 |
43 | Headers []Header `json:"headers" gorm:"constraint:OnDelete:CASCADE"`
44 | Network []NetworkLog `json:"network" gorm:"constraint:OnDelete:CASCADE"`
45 | Console []ConsoleLog `json:"console" gorm:"constraint:OnDelete:CASCADE"`
46 | Cookies []Cookie `json:"cookies" gorm:"constraint:OnDelete:CASCADE"`
47 | }
48 |
49 | func (r *Result) HeaderMap() map[string][]string {
50 | headersMap := make(map[string][]string)
51 |
52 | for _, header := range r.Headers {
53 | headersMap[header.Key] = []string{header.Value}
54 | }
55 |
56 | return headersMap
57 | }
58 |
59 | type TLS struct {
60 | ID uint `json:"id" gorm:"primarykey"`
61 | ResultID uint `json:"resultid"`
62 |
63 | Protocol string `json:"protocol"`
64 | KeyExchange string `json:"key_exchange"`
65 | Cipher string `json:"cipher"`
66 | SubjectName string `json:"subject_name"`
67 | SanList []TLSSanList `json:"san_list" gorm:"constraint:OnDelete:CASCADE"`
68 | Issuer string `json:"issuer"`
69 | ValidFrom time.Time `json:"valid_from"`
70 | ValidTo time.Time `json:"valid_to"`
71 | ServerSignatureAlgorithm int64 `json:"server_signature_algorithm"`
72 | EncryptedClientHello bool `json:"encrypted_client_hello"`
73 | }
74 |
75 | type TLSSanList struct {
76 | ID uint `json:"id" gorm:"primarykey"`
77 | TLSID uint `json:"tls_id"`
78 |
79 | Value string `json:"value"`
80 | }
81 |
82 | type Technology struct {
83 | ID uint `json:"id" gorm:"primarykey"`
84 | ResultID uint `json:"result_id"`
85 |
86 | Value string `json:"value" gorm:"index"`
87 | }
88 |
89 | type Header struct {
90 | ID uint `json:"id" gorm:"primarykey"`
91 | ResultID uint `json:"result_id"`
92 |
93 | Key string `json:"key"`
94 | Value string `json:"value" gorm:"type:longtext;index:,length:191"`
95 | }
96 |
97 | type NetworkLog struct {
98 | ID uint `json:"id" gorm:"primarykey"`
99 | ResultID uint `json:"result_id"`
100 |
101 | RequestType RequestType `json:"request_type"`
102 | StatusCode int64 `json:"status_code"`
103 | URL string `json:"url"`
104 | RemoteIP string `json:"remote_ip"`
105 | MIMEType string `json:"mime_type"`
106 | Time time.Time `json:"time"`
107 | Content []byte `json:"content"`
108 | Error string `json:"error"`
109 | }
110 |
111 | type ConsoleLog struct {
112 | ID uint `json:"id" gorm:"primarykey"`
113 | ResultID uint `json:"result_id"`
114 |
115 | Type string `json:"type"`
116 | Value string `json:"value" gorm:"type:longtext;index:,length:191"`
117 | }
118 |
119 | type Cookie struct {
120 | ID uint `json:"id" gorm:"primarykey"`
121 | ResultID uint `json:"result_id"`
122 |
123 | Name string `json:"name"`
124 | Value string `json:"value"`
125 | Domain string `json:"domain"`
126 | Path string `json:"path"`
127 | Expires time.Time `json:"expires"`
128 | Size int64 `json:"size"`
129 | HTTPOnly bool `json:"http_only"`
130 | Secure bool `json:"secure"`
131 | Session bool `json:"session"`
132 | Priority string `json:"priority"`
133 | SourceScheme string `json:"source_scheme"`
134 | SourcePort int64 `json:"source_port"`
135 | }
136 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/sensepost/gowitness
2 |
3 | go 1.25
4 |
5 | require (
6 | github.com/charmbracelet/glamour v0.10.0
7 | github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
8 | github.com/charmbracelet/log v0.4.2
9 | github.com/charmbracelet/x/term v0.2.2
10 | github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
11 | github.com/chromedp/chromedp v0.14.2
12 | github.com/corona10/goimagehash v1.1.0
13 | github.com/glebarez/sqlite v1.11.0
14 | github.com/go-chi/chi/v5 v5.2.3
15 | github.com/go-chi/cors v1.2.2
16 | github.com/go-rod/rod v0.116.2
17 | github.com/lair-framework/go-nmap v0.0.0-20191202052157-3507e0b03523
18 | github.com/projectdiscovery/wappalyzergo v0.2.56
19 | github.com/spf13/cobra v1.10.1
20 | github.com/swaggo/http-swagger v1.3.4
21 | github.com/swaggo/swag v1.16.6
22 | github.com/ysmood/gson v0.7.3
23 | gorm.io/driver/mysql v1.6.0
24 | gorm.io/driver/postgres v1.6.0
25 | gorm.io/gorm v1.31.1
26 | )
27 |
28 | require (
29 | filippo.io/edwards25519 v1.1.0 // indirect
30 | github.com/KyleBanks/depth v1.2.1 // indirect
31 | github.com/alecthomas/chroma/v2 v2.20.0 // indirect
32 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
33 | github.com/aymerick/douceur v0.2.0 // indirect
34 | github.com/charmbracelet/colorprofile v0.3.3 // indirect
35 | github.com/charmbracelet/x/ansi v0.11.1 // indirect
36 | github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
37 | github.com/charmbracelet/x/exp/slice v0.0.0-20251118172736-77d017256798 // indirect
38 | github.com/chromedp/sysutil v1.1.0 // indirect
39 | github.com/clipperhouse/displaywidth v0.6.0 // indirect
40 | github.com/clipperhouse/stringish v0.1.1 // indirect
41 | github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
42 | github.com/dlclark/regexp2 v1.11.5 // indirect
43 | github.com/dustin/go-humanize v1.0.1 // indirect
44 | github.com/glebarez/go-sqlite v1.22.0 // indirect
45 | github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect
46 | github.com/go-logfmt/logfmt v0.6.1 // indirect
47 | github.com/go-openapi/jsonpointer v0.22.3 // indirect
48 | github.com/go-openapi/jsonreference v0.21.3 // indirect
49 | github.com/go-openapi/spec v0.22.1 // indirect
50 | github.com/go-openapi/swag/conv v0.25.3 // indirect
51 | github.com/go-openapi/swag/jsonname v0.25.3 // indirect
52 | github.com/go-openapi/swag/jsonutils v0.25.3 // indirect
53 | github.com/go-openapi/swag/loading v0.25.3 // indirect
54 | github.com/go-openapi/swag/stringutils v0.25.3 // indirect
55 | github.com/go-openapi/swag/typeutils v0.25.3 // indirect
56 | github.com/go-openapi/swag/yamlutils v0.25.3 // indirect
57 | github.com/go-sql-driver/mysql v1.9.3 // indirect
58 | github.com/gobwas/httphead v0.1.0 // indirect
59 | github.com/gobwas/pool v0.2.1 // indirect
60 | github.com/gobwas/ws v1.4.0 // indirect
61 | github.com/google/uuid v1.6.0 // indirect
62 | github.com/gorilla/css v1.0.1 // indirect
63 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
64 | github.com/jackc/pgpassfile v1.0.0 // indirect
65 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
66 | github.com/jackc/pgx/v5 v5.7.6 // indirect
67 | github.com/jackc/puddle/v2 v2.2.2 // indirect
68 | github.com/jinzhu/inflection v1.0.0 // indirect
69 | github.com/jinzhu/now v1.1.5 // indirect
70 | github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
71 | github.com/mattn/go-isatty v0.0.20 // indirect
72 | github.com/mattn/go-runewidth v0.0.19 // indirect
73 | github.com/microcosm-cc/bluemonday v1.0.27 // indirect
74 | github.com/muesli/reflow v0.3.0 // indirect
75 | github.com/muesli/termenv v0.16.0 // indirect
76 | github.com/ncruces/go-strftime v1.0.0 // indirect
77 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
78 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
79 | github.com/rivo/uniseg v0.4.7 // indirect
80 | github.com/spf13/pflag v1.0.10 // indirect
81 | github.com/swaggo/files v1.0.1 // indirect
82 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
83 | github.com/ysmood/fetchup v0.2.3 // indirect
84 | github.com/ysmood/goob v0.4.0 // indirect
85 | github.com/ysmood/got v0.42.0 // indirect
86 | github.com/ysmood/leakless v0.9.0 // indirect
87 | github.com/yuin/goldmark v1.7.13 // indirect
88 | github.com/yuin/goldmark-emoji v1.0.6 // indirect
89 | go.yaml.in/yaml/v3 v3.0.4 // indirect
90 | golang.org/x/crypto v0.45.0 // indirect
91 | golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 // indirect
92 | golang.org/x/mod v0.30.0 // indirect
93 | golang.org/x/net v0.47.0 // indirect
94 | golang.org/x/sync v0.18.0 // indirect
95 | golang.org/x/sys v0.38.0 // indirect
96 | golang.org/x/term v0.37.0 // indirect
97 | golang.org/x/text v0.31.0 // indirect
98 | golang.org/x/tools v0.39.0 // indirect
99 | modernc.org/libc v1.67.1 // indirect
100 | modernc.org/mathutil v1.7.1 // indirect
101 | modernc.org/memory v1.11.0 // indirect
102 | modernc.org/sqlite v1.40.1 // indirect
103 | )
104 |
--------------------------------------------------------------------------------
/pkg/readers/file.go:
--------------------------------------------------------------------------------
1 | package readers
2 |
3 | import (
4 | "bufio"
5 | "fmt"
6 | "net/url"
7 | "os"
8 | "strconv"
9 | "strings"
10 |
11 | "github.com/sensepost/gowitness/internal/islazy"
12 | )
13 |
14 | // FileReader is a reader that expects a file with targets that
15 | // is newline delimited.
16 | type FileReader struct {
17 | Options *FileReaderOptions
18 | }
19 |
20 | // FileReaderOptions are options for the file reader
21 | type FileReaderOptions struct {
22 | Source string
23 | NoHTTP bool
24 | NoHTTPS bool
25 | Ports []int
26 | PortsSmall bool
27 | PortsMedium bool
28 | PortsLarge bool
29 | Random bool
30 | }
31 |
32 | // NewFileReader prepares a new file reader
33 | func NewFileReader(opts *FileReaderOptions) *FileReader {
34 | return &FileReader{
35 | Options: opts,
36 | }
37 | }
38 |
39 | // Read from a file that contains targets.
40 | // FilePath can be "-" indicating that we should read from stdin.
41 | func (fr *FileReader) Read(ch chan<- string) error {
42 | defer close(ch)
43 |
44 | var file *os.File
45 | var err error
46 |
47 | if fr.Options.Source == "-" {
48 | file = os.Stdin
49 | } else {
50 | file, err = os.Open(fr.Options.Source)
51 | if err != nil {
52 | return err
53 | }
54 | defer file.Close()
55 | }
56 |
57 | // determine any ports
58 | ports := fr.ports()
59 |
60 | scanner := bufio.NewScanner(file)
61 | for scanner.Scan() {
62 | candidate := scanner.Text()
63 | if candidate == "" {
64 | continue
65 | }
66 |
67 | for _, url := range fr.urlsFor(candidate, ports) {
68 | ch <- url
69 | }
70 | }
71 |
72 | return scanner.Err()
73 | }
74 |
75 | // urlsFor returns URLs for a scanning candidate.
76 | //
77 | // For candidates with no protocol, (and none of http/https is ignored), the
78 | // method will return two urls.
79 | // If any ports configuration exists, those will also be added as candidates.
80 | func (fr *FileReader) urlsFor(candidate string, ports []int) []string {
81 | var urls []string
82 |
83 | // trim any spaces
84 | candidate = strings.TrimSpace(candidate)
85 |
86 | // check if we got a scheme, add
87 | hasScheme := strings.Contains(candidate, "://")
88 | if !hasScheme {
89 | candidate = "http://" + candidate
90 | }
91 |
92 | parsedURL, err := url.Parse(candidate)
93 | if err != nil {
94 | // invalid url, return empty slice
95 | return urls
96 | }
97 |
98 | hasPort := parsedURL.Port() != ""
99 | hostname := parsedURL.Hostname()
100 |
101 | // if hostname is not set we may have rubbish input. try and "fix" it
102 | if hostname == "" {
103 | // is it hostname/path?
104 | if idx := strings.Index(candidate, "/"); idx != -1 {
105 | parsedURL.Host = candidate[:idx]
106 | parsedURL.Path = candidate[idx:]
107 | hostname = parsedURL.Hostname()
108 | } else {
109 | // its just a hostname then?
110 | parsedURL.Host = candidate
111 | parsedURL.Path = ""
112 | hostname = candidate
113 | }
114 |
115 | // at this point if hostname is still "", then just skip it entirely
116 | if hostname == "" {
117 | return urls
118 | }
119 | }
120 |
121 | if hasScheme && hasPort {
122 | // return the candidate as is
123 | urls = append(urls, parsedURL.String())
124 | return urls
125 | }
126 |
127 | // determine schemes to apply
128 | var schemes []string
129 | if hasScheme {
130 | schemes = append(schemes, parsedURL.Scheme)
131 | } else {
132 | if !fr.Options.NoHTTP {
133 | schemes = append(schemes, "http")
134 | }
135 | if !fr.Options.NoHTTPS {
136 | schemes = append(schemes, "https")
137 | }
138 | }
139 |
140 | // determine ports to use
141 | var targetPorts []int
142 | if hasPort {
143 | port, err := strconv.Atoi(parsedURL.Port())
144 | if err == nil { // just ignore it
145 | targetPorts = append(targetPorts, port)
146 | }
147 | } else {
148 | // If no port is specified, use the provided ports
149 | targetPorts = ports
150 | }
151 |
152 | // generate the urls
153 | for _, scheme := range schemes {
154 | for _, port := range targetPorts {
155 | host := hostname
156 |
157 | if port != 0 {
158 | if isIPv6(hostname) {
159 | host = fmt.Sprintf("[%s]:%d", hostname, port)
160 | } else {
161 | host = fmt.Sprintf("%s:%d", hostname, port)
162 | }
163 | }
164 |
165 | fullURL := url.URL{
166 | Scheme: scheme,
167 | Host: host,
168 | Path: parsedURL.Path,
169 | RawQuery: parsedURL.RawQuery,
170 | }
171 |
172 | urls = append(urls, fullURL.String())
173 | }
174 | }
175 |
176 | return urls
177 | }
178 |
179 | // ports returns all of the ports to scan
180 | func (fr *FileReader) ports() []int {
181 | var ports = fr.Options.Ports
182 |
183 | if fr.Options.PortsSmall {
184 | ports = append(ports, small...)
185 | }
186 |
187 | if fr.Options.PortsMedium {
188 | ports = append(ports, medium...)
189 | }
190 |
191 | if fr.Options.PortsLarge {
192 | ports = append(ports, large...)
193 | }
194 |
195 | return islazy.UniqueIntSlice(ports)
196 | }
197 |
198 | func isIPv6(hostname string) bool {
199 | return len(hostname) > 0 && hostname[0] == '[' && hostname[len(hostname)-1] == ']'
200 | }
201 |
--------------------------------------------------------------------------------
/web/api/gallery.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 | "strconv"
7 | "strings"
8 | "time"
9 |
10 | "github.com/sensepost/gowitness/pkg/log"
11 | "github.com/sensepost/gowitness/pkg/models"
12 | )
13 |
14 | type galleryResponse struct {
15 | Results []*galleryContent `json:"results"`
16 | Page int `json:"page"`
17 | Limit int `json:"limit"`
18 | TotalCount int64 `json:"total_count"`
19 | }
20 |
21 | type galleryContent struct {
22 | ID uint `json:"id"`
23 | ProbedAt time.Time `json:"probed_at"`
24 | URL string `json:"url"`
25 | ResponseCode int `json:"response_code"`
26 | Title string `json:"title"`
27 | Filename string `json:"file_name"`
28 | Screenshot string `json:"screenshot"`
29 | Failed bool `json:"failed"`
30 | Technologies []string `json:"technologies"`
31 | }
32 |
33 | // GalleryHandler gets a paginated gallery
34 | //
35 | // @Summary Gallery
36 | // @Description Get a paginated list of results.
37 | // @Tags Results
38 | // @Accept json
39 | // @Produce json
40 | // @Param page query int false "The page to load."
41 | // @Param limit query int false "Number of results per page."
42 | // @Param technologies query string false "A comma seperated list of technologies to filter by."
43 | // @Param status query string false "A comma seperated list of HTTP status codes to filter by."
44 | // @Param perception query boolean false "Order the results by perception hash."
45 | // @Param failed query boolean false "Include failed screenshots in the results."
46 | // @Success 200 {object} galleryResponse
47 | // @Router /results/gallery [get]
48 | func (h *ApiHandler) GalleryHandler(w http.ResponseWriter, r *http.Request) {
49 | var results = &galleryResponse{
50 | Page: 1,
51 | Limit: 24,
52 | }
53 |
54 | // pagination
55 | urlPage := r.URL.Query().Get("page")
56 | urlLimit := r.URL.Query().Get("limit")
57 | if p, err := strconv.Atoi(urlPage); err == nil && p > 0 {
58 | results.Page = p
59 | }
60 | if l, err := strconv.Atoi(urlLimit); err == nil && l > 0 {
61 | results.Limit = l
62 | }
63 | offset := (results.Page - 1) * results.Limit
64 |
65 | // perception sorting
66 | var perceptionSort bool
67 | perceptionSortValue := r.URL.Query().Get("perception")
68 | perceptionSort, err := strconv.ParseBool(perceptionSortValue)
69 | if err != nil {
70 | perceptionSort = false
71 | }
72 |
73 | // status code filtering
74 | var statusCodes []int
75 | statusFilterValue := r.URL.Query().Get("status")
76 | if statusFilterValue != "" {
77 | for _, statusCodeString := range strings.Split(statusFilterValue, ",") {
78 | statusCode, err := strconv.Atoi(statusCodeString)
79 | if err != nil {
80 | continue
81 | }
82 |
83 | statusCodes = append(statusCodes, statusCode)
84 | }
85 | }
86 |
87 | // technology filtering
88 | var technologies []string
89 | technologyFilterValue := r.URL.Query().Get("technologies")
90 | if technologyFilterValue != "" {
91 | technologies = append(technologies, strings.Split(technologyFilterValue, ",")...)
92 | }
93 |
94 | // failed result filtering
95 | var showFailed bool
96 | showFailed, err = strconv.ParseBool(r.URL.Query().Get("failed"))
97 | if err != nil {
98 | showFailed = true
99 | }
100 |
101 | // query the db
102 | var queryResults []*models.Result
103 | query := h.DB.Model(&models.Result{}).Limit(results.Limit).
104 | Offset(offset).Preload("Technologies")
105 |
106 | if perceptionSort {
107 | query.Order("perception_hash_group_id DESC")
108 | }
109 |
110 | if len(statusCodes) > 0 {
111 | query.Where("response_code IN ?", statusCodes)
112 | }
113 |
114 | if len(technologies) > 0 {
115 | query.Where("id in (?)", h.DB.Model(&models.Technology{}).
116 | Select("result_id").Distinct("result_id").
117 | Where("value IN (?)", technologies))
118 | }
119 |
120 | if !showFailed {
121 | query.Where("failed = ?", showFailed)
122 | }
123 |
124 | // run the query
125 | if err := query.Find(&queryResults).Error; err != nil {
126 | log.Error("could not get gallery", "err", err)
127 | return
128 | }
129 |
130 | // extract Technologies for each result
131 | for _, result := range queryResults {
132 | var technologies []string
133 | for _, tech := range result.Technologies {
134 | technologies = append(technologies, tech.Value)
135 | }
136 |
137 | // Append the processed data to the response
138 | results.Results = append(results.Results, &galleryContent{
139 | ID: result.ID,
140 | ProbedAt: result.ProbedAt,
141 | URL: result.URL,
142 | ResponseCode: result.ResponseCode,
143 | Title: result.Title,
144 | Filename: result.Filename,
145 | Screenshot: result.Screenshot,
146 | Failed: result.Failed,
147 | Technologies: technologies,
148 | })
149 | }
150 |
151 | if err := h.DB.Model(&models.Result{}).Count(&results.TotalCount).Error; err != nil {
152 | log.Error("could not count total results", "err", err)
153 | return
154 | }
155 |
156 | jsonData, err := json.Marshal(results)
157 | if err != nil {
158 | http.Error(w, err.Error(), http.StatusInternalServerError)
159 | return
160 | }
161 |
162 | w.Write(jsonData)
163 | }
164 |
--------------------------------------------------------------------------------
|