├── .github └── workflows │ └── goreleaser.yml ├── .gitignore ├── .goreleaser.yaml ├── README.md ├── cmd ├── db_import │ └── main.go └── db_nmap │ ├── main.go │ ├── nmap_run.go │ └── usage.txt ├── go.mod ├── go.sum └── internal ├── connection.go ├── glue.go ├── glue_test.go ├── logging.go ├── msf_db.go ├── nmap_xml.go └── testdata ├── localhost.xml └── scanme.xml /.github/workflows/goreleaser.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "*" 8 | 9 | jobs: 10 | release: 11 | name: GoReleaser build 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: 1.19 22 | 23 | - name: GoReleaser Action 24 | uses: goreleaser/goreleaser-action@v4.2.0 25 | with: 26 | version: latest 27 | args: release --clean 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | /test* 3 | /db_nmap 4 | /db_import 5 | dist/ 6 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod tidy 7 | # you may remove this if you don't need go generate 8 | # - go generate ./... 9 | builds: 10 | - id: db_nmap 11 | main: ./cmd/db_nmap 12 | binary: db_nmap 13 | env: 14 | - CGO_ENABLED=0 15 | goos: 16 | - linux 17 | # - windows 18 | # - darwin 19 | 20 | - id: db_import 21 | main: ./cmd/db_import 22 | binary: db_import 23 | env: 24 | - CGO_ENABLED=0 25 | goos: 26 | - linux 27 | # - windows 28 | # - darwin 29 | 30 | archives: 31 | - format: tar.gz 32 | # this name template makes the OS and Arch compatible with the results of uname. 33 | name_template: >- 34 | {{ .ProjectName }}_ 35 | {{- title .Os }}_ 36 | {{- if eq .Arch "amd64" }}x86_64 37 | {{- else if eq .Arch "386" }}i386 38 | {{- else }}{{ .Arch }}{{ end }} 39 | {{- if .Arm }}v{{ .Arm }}{{ end }} 40 | # use zip for windows archives 41 | format_overrides: 42 | - goos: windows 43 | format: zip 44 | checksum: 45 | name_template: "checksums.txt" 46 | snapshot: 47 | name_template: "{{ incpatch .Version }}-next" 48 | changelog: 49 | sort: asc 50 | filters: 51 | exclude: 52 | - "^docs:" 53 | - "^test:" 54 | # The lines beneath this are called `modelines`. See `:help modeline` 55 | # Feel free to remove those if you don't want/use them. 56 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 57 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # db_nmap 2 | 3 | `db_nmap` and `db_import` are glue commands between [Nmap](https://nmap.org/) and [Metasploit](https://www.metasploit.com/): 4 | 5 | - `db_nmap` is a wrapper around Nmap that inserts Nmap's results into the Metasploit PostgreSQL database, right after they are finished scanning. 6 | - `db_import` is a standalone program that takes an Nmap result XML document and inserts the results into the Metasploit PostgreSQL daabase. 7 | 8 | After importing the results, they can be inspected with the Metasploit console commands `services` and `hosts`. 9 | 10 | Both commands are actually standalone implementations of the corresponding commands in Metasploit, which are documented [here (`db_nmap`)](https://www.offensive-security.com/metasploit-unleashed/port-scanning/) and [here (`db_import`)](https://www.offensive-security.com/metasploit-unleashed/using-databases/). 11 | 12 | ## Example 13 | 14 | On my local system, I am currently only running a PostgreSQL database (for Metasploit). I can scan `localhost` with the following command: 15 | 16 | $ db_nmap -sV 127.0.0.1 17 | INFO[2021-10-19 18:47:52] Connected to Metasploit PostgreSQL database "msf" at 127.0.0.1:5432 as user "jonas" 18 | DEBU[2021-10-19 18:47:52] ID of Metasploit workspace "default": 1 19 | DEBU[2021-10-19 18:47:52] Running "/usr/bin/nmap -sV 127.0.0.1 -oX /dev/fd/3" ... 20 | Starting Nmap 7.92 ( https://nmap.org ) at 2021-10-19 18:47 CEST 21 | Nmap scan report for localhost (127.0.0.1) 22 | Host is up (0.0000040s latency). 23 | Not shown: 999 closed tcp ports (reset) 24 | PORT STATE SERVICE VERSION 25 | 5432/tcp open postgresql PostgreSQL DB 9.6.0 or later 26 | 1 service unrecognized despite returning data. [...] 27 | 28 | Service detection performed. Please report any incorrect results at https://nmap.org/submit/ . 29 | Nmap done: 1 IP address (1 host up) scanned in 6.48 seconds 30 | DEBU[2021-10-19 18:47:59] Inserted/updated host 127.0.0.1 (localhost). 31 | DEBU[2021-10-19 18:47:59] Inserted/updated service tcp:5432 (PostgreSQL DB). 32 | DEBU[2021-10-19 18:47:59] XML document complete. 33 | INFO[2021-10-19 18:47:59] Wrapper stats: registered 1 hosts with 1 services. 34 | 35 | Note that `db_nmap` used the `nmap` command found in my `PATH`. 36 | After scanning, the results can be retrieved from the Metasploit database: 37 | 38 | $ msfconsole 39 | [...] 40 | 41 | msf6 > hosts -c address,name 42 | 43 | Hosts 44 | ===== 45 | 46 | address name 47 | ------- ---- 48 | 127.0.0.1 localhost 49 | 50 | msf6 > services 51 | Services 52 | ======== 53 | 54 | host port proto name state info 55 | ---- ---- ----- ---- ----- ---- 56 | 127.0.0.1 5432 tcp postgresql open PostgreSQL DB 57 | 58 | ## Passing options 59 | 60 | Options, such as the database host, port, user and password can be passed through PostgreSQL's default environment variables documented [here](https://www.postgresql.org/docs/current/libpq-envars.html), e.g.: 61 | 62 | $ PGUSER=metasploit PGPASSWORD=secret db_nmap -sV 127.0.0.1 63 | 64 | In addition, the Metasploit workspace name can be set with the environment variable `MSF_WORKSPACE` (default: `default`): 65 | 66 | $ MSF_WORKSPACE=project2 db_nmap -sV 127.0.0.1 67 | 68 | ## Building 69 | 70 | The project is implemented in Go and can be built as follows: 71 | 72 | go build ./cmd/db_nmap 73 | go build ./cmd/db_import 74 | -------------------------------------------------------------------------------- /cmd/db_import/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/jojonas/db_nmap/internal" 9 | ) 10 | 11 | var log = internal.Logger 12 | var version string = "dev" 13 | 14 | func main() { 15 | if len(os.Args) < 2 { 16 | fmt.Fprintf(os.Stderr, "Usage: %s XMLFILE [XMLFILE...]\n", os.Args[0]) 17 | os.Exit(1) 18 | } 19 | 20 | log.Infof("db_import %s starting...", version) 21 | 22 | ctx := context.Background() 23 | 24 | db, workspaceId, err := internal.Connect(ctx) 25 | if err != nil { 26 | log.Fatalf("Error: %v", err) 27 | } 28 | 29 | hostCount := 0 30 | serviceCount := 0 31 | 32 | for _, filename := range os.Args[1:] { 33 | file, err := os.Open(filename) 34 | if err != nil { 35 | log.Errorf("Opening %q: %v", filename, err) 36 | continue 37 | } 38 | 39 | err = internal.ParseNmapXML(file, func(host internal.NmapHost) error { 40 | n, err := internal.InsertHost(db, int(workspaceId), host) 41 | 42 | if err != nil { 43 | log.Warnf("Inserting host into DB: %v", err) 44 | return nil 45 | } 46 | 47 | if n > 0 { 48 | hostCount += 1 49 | serviceCount += n 50 | } 51 | 52 | return nil 53 | }) 54 | 55 | if err != nil { 56 | log.Errorf("Parsing %q: %v", filename, err) 57 | } 58 | } 59 | 60 | log.Infof("Import stats: registered %d hosts with %d services.", hostCount, serviceCount) 61 | } 62 | -------------------------------------------------------------------------------- /cmd/db_nmap/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/exec" 7 | "path/filepath" 8 | "strings" 9 | "text/template" 10 | 11 | _ "embed" 12 | 13 | "github.com/jojonas/db_nmap/internal" 14 | ) 15 | 16 | var log = internal.Logger 17 | var binaryPath = "nmap" 18 | var version string = "dev" 19 | 20 | func main() { 21 | if hasArgument(os.Args, "--help") || hasArgument(os.Args, "-h") { 22 | usage() 23 | } 24 | 25 | log.Infof("db_nmap %s starting...", version) 26 | 27 | ctx := context.Background() 28 | 29 | db, workspaceId, err := internal.Connect(ctx) 30 | if err != nil { 31 | log.Fatalf("Error: %v", err) 32 | } 33 | 34 | cmd := exec.CommandContext(ctx, binaryPath, os.Args[1:]...) 35 | 36 | cmd.Stdout = os.Stdout 37 | cmd.Stderr = os.Stderr 38 | 39 | hostCount := 0 40 | serviceCount := 0 41 | err = runNmap(cmd, func(host internal.NmapHost) error { 42 | n, err := internal.InsertHost(db, int(workspaceId), host) 43 | 44 | if err != nil { 45 | log.Warnf("Inserting host into DB: %v", err) 46 | return nil 47 | } 48 | 49 | if n > 0 { 50 | hostCount += 1 51 | serviceCount += n 52 | } 53 | 54 | return nil 55 | }) 56 | 57 | log.Infof("Wrapper stats: registered %d hosts with %d services.", hostCount, serviceCount) 58 | 59 | if err != nil { 60 | log.Errorf("%v", err) 61 | } 62 | 63 | os.Exit(cmd.ProcessState.ExitCode()) 64 | } 65 | 66 | //go:embed usage.txt 67 | var usageTemplate string 68 | 69 | func usage() { 70 | vars := struct { 71 | Name string 72 | ConnString string 73 | WorkspaceEnvVar string 74 | TestedVersions []string 75 | Version string 76 | }{ 77 | Name: filepath.Base(os.Args[0]), 78 | ConnString: internal.ConnString, 79 | WorkspaceEnvVar: internal.WorkspaceEnvVar, 80 | TestedVersions: internal.TestedVersions, 81 | Version: version, 82 | } 83 | 84 | tmpl := template.New("usage.txt") 85 | tmpl = tmpl.Funcs(template.FuncMap{"join": strings.Join}) 86 | tmpl, err := tmpl.Parse(usageTemplate) 87 | 88 | if err != nil { 89 | log.Errorf("Parsing template usage.txt: %v", err) 90 | return 91 | } 92 | 93 | err = tmpl.Execute(os.Stdout, vars) 94 | if err != nil { 95 | log.Errorf("Executing template usage.txt: %v", err) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /cmd/db_nmap/nmap_run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | 12 | "github.com/jojonas/db_nmap/internal" 13 | ) 14 | 15 | func ensureArgument(args []string, arguments ...string) []string { 16 | searchArg := arguments[0] 17 | 18 | for i, a := range args { 19 | // find the first argument in args 20 | if a == searchArg { 21 | // if found, copy all arguments into the args array 22 | for j, s2 := range arguments { 23 | args[i+j] = s2 24 | } 25 | return args 26 | } 27 | } 28 | 29 | return append(args, arguments...) 30 | } 31 | 32 | func findArgument(args []string, argument string) string { 33 | for i, a := range args { 34 | if a == argument { 35 | if i+1 < len(args) { 36 | return args[i+1] 37 | } 38 | } 39 | } 40 | return "" 41 | } 42 | 43 | func hasArgument(args []string, argument string) bool { 44 | for _, a := range args { 45 | if a == argument { 46 | return true 47 | } 48 | } 49 | return false 50 | } 51 | 52 | func findOutputFile(args []string) string { 53 | output := findArgument(args, "-oX") 54 | if output != "" { 55 | return output 56 | } 57 | 58 | output = findArgument(args, "-oA") 59 | if output != "" { 60 | return output + ".xml" 61 | } 62 | 63 | return "" 64 | } 65 | 66 | func runNmap(cmd *exec.Cmd, handle internal.HandleHostFunc) error { 67 | resumeFilename := findArgument(cmd.Args, "--resume") 68 | isResume := resumeFilename != "" 69 | 70 | outputFilename := "" 71 | 72 | if isResume { 73 | log.Debugf("Resuming from file %q.", resumeFilename) 74 | outputFilename = strings.TrimSuffix(resumeFilename, filepath.Ext(resumeFilename)) + ".xml" 75 | } else { 76 | outputFilename = findOutputFile(cmd.Args) 77 | cmd.Args = ensureArgument(cmd.Args, "-oX", "/dev/fd/3") 78 | } 79 | 80 | var readerPipe io.Reader 81 | readerPipe, writerPipe, err := os.Pipe() 82 | if err != nil { 83 | return fmt.Errorf("creating reader/writer pipe: %w", err) 84 | } 85 | defer writerPipe.Close() 86 | 87 | if outputFilename != "" { 88 | log.Debugf("Teeing to file %q.", outputFilename) 89 | 90 | var outputFile *os.File 91 | if isResume { 92 | outputFile, err = os.OpenFile(outputFilename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) 93 | if err != nil { 94 | return fmt.Errorf("opening %q for appending: %w", outputFilename, err) 95 | } 96 | } else { 97 | outputFile, err = os.Create(outputFilename) 98 | if err != nil { 99 | return fmt.Errorf("opening %v for writing: %w", outputFilename, err) 100 | } 101 | } 102 | defer outputFile.Close() 103 | 104 | readerPipe = io.TeeReader(readerPipe, outputFile) 105 | } 106 | 107 | cmd.ExtraFiles = []*os.File{writerPipe} 108 | 109 | var wg sync.WaitGroup 110 | 111 | wg.Add(1) 112 | go func() { 113 | err := internal.ParseNmapXML(readerPipe, handle) 114 | if err != nil { 115 | fmt.Fprintf(os.Stderr, "Error watching XML: %v\n", err) 116 | } 117 | wg.Done() 118 | }() 119 | 120 | log.Debugf("Running %q ...", cmd) 121 | err = cmd.Run() 122 | writerPipe.Close() 123 | 124 | if err != nil { 125 | return fmt.Errorf("running command %q: %w", cmd, err) 126 | } 127 | 128 | wg.Wait() 129 | 130 | return nil 131 | } 132 | -------------------------------------------------------------------------------- /cmd/db_nmap/usage.txt: -------------------------------------------------------------------------------- 1 | {{.Name}} {{.Version}} 2 | 3 | Usage: {{.Name}} 4 | 5 | {{.Name}} is a wrapper around Nmap that inserts hosts and services into a 6 | Metasploit database right after the corresponding host group has been scanned. 7 | It does so by reading Nmap's XML output and parsing it as a stream of hosts. 8 | {{if ne .ConnString ""}} 9 | The default database connection string defined at compile time is: 10 | {{.ConnString}} 11 | {{end}} 12 | The default database settings can be overriden by specifying environment 13 | variables such as PGHOST, PGPORT, PGUSER or PGPASSWORD. For a full list of 14 | options, see: 15 | https://www.postgresql.org/docs/current/libpq-envars.html 16 | 17 | The destination workspace can be configured with the environment variable 18 | {{.WorkspaceEnvVar}}. 19 | 20 | {{.Name}} {{.Version}} was tested with Nmap versions: {{join .TestedVersions ", "}} 21 | 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jojonas/db_nmap 2 | 3 | go 1.21 4 | 5 | toolchain go1.22.2 6 | 7 | require ( 8 | github.com/jackc/pgx/v4 v4.18.3 9 | github.com/sirupsen/logrus v1.9.3 10 | gorm.io/driver/postgres v1.5.9 11 | gorm.io/gorm v1.25.12 12 | ) 13 | 14 | require ( 15 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 16 | github.com/jackc/pgconn v1.14.3 // indirect 17 | github.com/jackc/pgio v1.0.0 // indirect 18 | github.com/jackc/pgpassfile v1.0.0 // indirect 19 | github.com/jackc/pgproto3/v2 v2.3.3 // indirect 20 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 21 | github.com/jackc/pgtype v1.14.4 // indirect 22 | github.com/jackc/pgx/v5 v5.7.1 // indirect 23 | github.com/jackc/puddle/v2 v2.2.2 // indirect 24 | github.com/jinzhu/inflection v1.0.0 // indirect 25 | github.com/jinzhu/now v1.1.5 // indirect 26 | github.com/lib/pq v1.10.4 // indirect 27 | github.com/pkg/errors v0.9.1 // indirect 28 | golang.org/x/crypto v0.28.0 // indirect 29 | golang.org/x/sync v0.8.0 // indirect 30 | golang.org/x/sys v0.26.0 // indirect 31 | golang.org/x/text v0.19.0 // indirect 32 | gopkg.in/yaml.v3 v3.0.1 // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= 3 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 4 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 5 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 6 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 7 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 8 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 13 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 14 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 15 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 16 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 17 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 18 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 19 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 20 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 21 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 22 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 23 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 24 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 25 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 26 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 27 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 28 | github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= 29 | github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= 30 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 31 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 32 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 33 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 34 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 35 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 36 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 37 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 38 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 39 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 40 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 41 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 42 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 43 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 44 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 45 | github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= 46 | github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 47 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 48 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 49 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 50 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 51 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 52 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 53 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 54 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 55 | github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 56 | github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= 57 | github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= 58 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 59 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 60 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 61 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 62 | github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= 63 | github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= 64 | github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= 65 | github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= 66 | github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= 67 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 68 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 69 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 70 | github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 71 | github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 72 | github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 73 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 74 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 75 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 76 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 77 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 78 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 79 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 80 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 81 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 82 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 83 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 84 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 85 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 86 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 87 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 88 | github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= 89 | github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 90 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 91 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 92 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 93 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 94 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 95 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 96 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 97 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 98 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 99 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 100 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 101 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 102 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 103 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 104 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 105 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 106 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 107 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 108 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 109 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 110 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 111 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 112 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 113 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 114 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 115 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 116 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 117 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 118 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 119 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 120 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 121 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 122 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 123 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 124 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 125 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 126 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 127 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 128 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 129 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 130 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 131 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 132 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 133 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 134 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 135 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 136 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 137 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 138 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 139 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 140 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 141 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 142 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 143 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 144 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 145 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 146 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 147 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 148 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 149 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 150 | golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= 151 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 152 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 153 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 154 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 155 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 156 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 157 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 158 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 159 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 160 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 161 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 162 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 163 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 164 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 165 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 166 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 167 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 168 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 169 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 170 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 171 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 172 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 173 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 174 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 175 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 177 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 178 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 179 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 180 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 181 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 182 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 183 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 184 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 185 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 186 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 187 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 188 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 189 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 190 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 191 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 192 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 193 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 194 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 195 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 196 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 197 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 198 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 199 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 200 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 201 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 202 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 203 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 204 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 205 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 206 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 207 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 208 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 209 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 210 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 211 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 212 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 213 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 214 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 215 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 216 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 217 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 218 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 219 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 220 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 221 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 222 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 223 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 224 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 225 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 226 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 227 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 228 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 229 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 230 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 231 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 232 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 233 | gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= 234 | gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= 235 | gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= 236 | gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= 237 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 238 | -------------------------------------------------------------------------------- /internal/connection.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/jackc/pgx/v4" 9 | "github.com/jackc/pgx/v4/stdlib" 10 | "gopkg.in/yaml.v3" 11 | "gorm.io/driver/postgres" 12 | "gorm.io/gorm" 13 | ) 14 | 15 | var ConnString = "" 16 | var MetasploitDatabaseConfigurationFile = "/usr/share/metasploit-framework/config/database.yml" 17 | 18 | const WorkspaceEnvVar = "MSF_WORKSPACE" 19 | 20 | type MetasploitDatabaseConfigSection struct { 21 | Adapter string 22 | Database string 23 | Username string 24 | Password string 25 | Host string 26 | Port uint16 27 | Pool int 28 | Timeout int 29 | } 30 | 31 | type MetasploitDatabaseConfig struct { 32 | Development MetasploitDatabaseConfigSection 33 | Production MetasploitDatabaseConfigSection 34 | Test MetasploitDatabaseConfigSection 35 | } 36 | 37 | func Connect(ctx context.Context) (*gorm.DB, int, error) { 38 | var err error 39 | var pgxCfg *pgx.ConnConfig 40 | 41 | if ConnString != "" { 42 | log.Infof("Connecting with PostgreSQL connection string: %q", ConnString) 43 | 44 | pgxCfg, err = pgx.ParseConfig(ConnString) 45 | if err != nil { 46 | return nil, 0, fmt.Errorf("parsing PostgreSQL connection string %q: %w", ConnString, err) 47 | } 48 | } else { 49 | pgxCfg, err = readMetasploitConfiguration(MetasploitDatabaseConfigurationFile) 50 | 51 | if err != nil { 52 | if os.IsNotExist(err) { 53 | log.Info("Creating default config...") 54 | pgxCfg, err = pgx.ParseConfig("") 55 | if err != nil { 56 | return nil, 0, fmt.Errorf("creating default config: %w", err) 57 | } 58 | } else { 59 | return nil, 0, fmt.Errorf("parsing Metasploit database configuration file %q: %w", MetasploitDatabaseConfigurationFile, err) 60 | } 61 | } else { 62 | log.Infof("Read Metasploit database configuration file %q.", MetasploitDatabaseConfigurationFile) 63 | } 64 | } 65 | 66 | log.Infof("Connecting to Metasploit PostgreSQL database %q at %s:%d as user %q", pgxCfg.Database, pgxCfg.Host, pgxCfg.Port, pgxCfg.User) 67 | 68 | sqlConn := stdlib.OpenDB(*pgxCfg) 69 | gormDb, err := gorm.Open(postgres.New(postgres.Config{Conn: sqlConn}), &gorm.Config{}) 70 | if err != nil { 71 | return nil, 0, fmt.Errorf("connect to PostgreSQL database: %w", err) 72 | } 73 | 74 | // use this to print all queries 75 | // gormDb = gormDb.Debug() 76 | 77 | workspace := os.Getenv(WorkspaceEnvVar) 78 | if workspace == "" { 79 | workspace = "default" 80 | } 81 | 82 | workspaceId, err := GetWorkspaceId(gormDb, workspace) 83 | if err != nil { 84 | return nil, 0, fmt.Errorf("reading ID of Metasploit workspace %q: %w", workspace, err) 85 | } 86 | 87 | log.Debugf("ID of Metasploit workspace %q: %d", workspace, workspaceId) 88 | 89 | return gormDb, workspaceId, nil 90 | } 91 | 92 | func readMetasploitConfiguration(filename string) (*pgx.ConnConfig, error) { 93 | data, err := os.ReadFile(filename) 94 | if err != nil { 95 | return nil, fmt.Errorf("reading %s: %w", filename, err) 96 | } 97 | 98 | msfConfig := MetasploitDatabaseConfig{} 99 | err = yaml.Unmarshal(data, &msfConfig) 100 | if err != nil { 101 | return nil, fmt.Errorf("parsing YAML in %s: %w", filename, err) 102 | } 103 | 104 | dbConfig, err := pgx.ParseConfig("") 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | dbConfig.Host = msfConfig.Production.Host 110 | dbConfig.Port = msfConfig.Production.Port 111 | dbConfig.Database = msfConfig.Production.Database 112 | dbConfig.User = msfConfig.Production.Username 113 | dbConfig.Password = msfConfig.Production.Password 114 | 115 | return dbConfig, nil 116 | } 117 | -------------------------------------------------------------------------------- /internal/glue.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/xml" 5 | "errors" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | var TestedVersions = []string{"7.40", "7.70", "7.80", "7.92", "7.93"} 11 | 12 | type HandleHostFunc func(host NmapHost) error 13 | 14 | func ParseNmapXML(reader io.Reader, handle HandleHostFunc) error { 15 | decoder := xml.NewDecoder(reader) 16 | 17 | outer: 18 | for { 19 | token, err := decoder.Token() 20 | 21 | if err != nil { 22 | if errors.Is(err, io.EOF) { 23 | log.Debug("Unexpected EOF") 24 | break outer 25 | } 26 | 27 | return fmt.Errorf("reading token: %w", err) 28 | } 29 | if token == nil { 30 | log.Debug("Unexpected end") 31 | break outer 32 | } 33 | 34 | switch t := token.(type) { 35 | case xml.StartElement: 36 | switch t.Name.Local { 37 | case "nmaprun": 38 | for _, attr := range t.Attr { 39 | if attr.Name.Local == "version" { 40 | checkVersion(attr.Value) 41 | break 42 | } 43 | } 44 | case "host": 45 | host := NmapHost{} 46 | err = decoder.DecodeElement(&host, &t) 47 | if err != nil { 48 | return fmt.Errorf("reading : %w", err) 49 | } 50 | 51 | err := handle(host) 52 | if err != nil { 53 | return fmt.Errorf("handling : %w", err) 54 | } 55 | } 56 | case xml.EndElement: 57 | switch t.Name.Local { 58 | case "nmaprun": 59 | log.Debug("XML document complete.") 60 | break outer 61 | } 62 | default: 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func checkVersion(version string) { 70 | isTested := false 71 | for _, testedVersions := range TestedVersions { 72 | if version == testedVersions { 73 | isTested = true 74 | break 75 | } 76 | } 77 | 78 | if !isTested { 79 | log.Warnf("This program was not tested against Nmap version %s.", version) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/glue_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/sirupsen/logrus" 9 | "github.com/sirupsen/logrus/hooks/test" 10 | ) 11 | 12 | func TestParse(t *testing.T) { 13 | log.SetLevel(logrus.DebugLevel) 14 | 15 | files := []string{"testdata/scanme.xml", "testdata/localhost.xml"} 16 | 17 | for _, filename := range files { 18 | t.Run(filename, func(t *testing.T) { 19 | reader, err := os.Open(filename) 20 | if err != nil { 21 | t.Fatalf("Error opening %q: %v", filename, err) 22 | } 23 | defer reader.Close() 24 | 25 | hosts := 0 26 | err = ParseNmapXML(reader, func(host NmapHost) error { 27 | hosts++ 28 | return nil 29 | }) 30 | 31 | if err != nil { 32 | t.Errorf("Error parsing %q: %v", filename, err) 33 | } 34 | 35 | if hosts == 0 { 36 | t.Error("No hosts found") 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestCheckVersion(t *testing.T) { 43 | var hook *test.Hook 44 | log, hook = test.NewNullLogger() 45 | 46 | version := "1.23" 47 | checkVersion(version) 48 | if !strings.Contains(hook.LastEntry().Message, "not tested") { 49 | t.Errorf("version %s does not trigger a log warning", version) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/logging.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/sirupsen/logrus" 5 | ) 6 | 7 | var Logger = logrus.New() 8 | var log = Logger 9 | 10 | func init() { 11 | log.SetLevel(logrus.DebugLevel) 12 | 13 | formatter := &logrus.TextFormatter{ 14 | TimestampFormat: "2006-01-02 15:04:05", 15 | FullTimestamp: true, 16 | } 17 | 18 | log.SetFormatter(formatter) 19 | } 20 | -------------------------------------------------------------------------------- /internal/msf_db.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "time" 7 | 8 | "gorm.io/gorm" 9 | "gorm.io/gorm/clause" 10 | ) 11 | 12 | type MsfWorkspace struct { 13 | Id int 14 | Name string 15 | Description string 16 | 17 | CreatedAt time.Time 18 | UpdatedAt time.Time 19 | } 20 | 21 | func (MsfWorkspace) TableName() string { 22 | return "workspaces" 23 | } 24 | 25 | type MsfHost struct { 26 | Id int 27 | WorkspaceId int 28 | Address string 29 | 30 | MAC string 31 | Name string 32 | State string 33 | OSName string 34 | Purpose string 35 | 36 | CreatedAt time.Time 37 | UpdatedAt time.Time 38 | } 39 | 40 | func (MsfHost) TableName() string { 41 | return "hosts" 42 | } 43 | 44 | type MsfService struct { 45 | Id int 46 | HostId int 47 | CreatedAt time.Time 48 | Port int 49 | Proto string 50 | State string 51 | Name string 52 | UpdatedAt time.Time 53 | Info string 54 | } 55 | 56 | func (MsfService) TableName() string { 57 | return "services" 58 | } 59 | 60 | func GetWorkspaceId(db *gorm.DB, workspaceName string) (int, error) { 61 | var workspace MsfWorkspace 62 | 63 | err := db.Where("name = ?", workspaceName).First(&workspace).Error 64 | if err != nil { 65 | return 0, fmt.Errorf("get id for workspace %q: %w", workspaceName, err) 66 | } 67 | 68 | return workspace.Id, nil 69 | } 70 | 71 | func InsertHost(db *gorm.DB, workspaceId int, nmapHost NmapHost) (int, error) { 72 | if !nmapHost.HasOpenPorts() { 73 | log.Debugf("Host %s does not have any open ports, skipping.", nmapHost) 74 | return 0, nil 75 | } 76 | 77 | now := time.Now() 78 | openPortCount := 0 79 | 80 | allIPs := nmapHost.AllIPAddresses() 81 | var preferredIP net.IP 82 | if len(allIPs) > 0 { 83 | preferredIP = net.ParseIP(allIPs[0].String()) 84 | } 85 | 86 | db.Transaction(func(tx *gorm.DB) error { 87 | var msfHost MsfHost 88 | 89 | msfHost.WorkspaceId = workspaceId 90 | msfHost.Address = preferredIP.String() 91 | 92 | err := tx. 93 | Clauses(clause.Locking{Strength: "UPDATE"}). 94 | Where("workspace_id = ? AND address = ?", msfHost.WorkspaceId, msfHost.Address). 95 | FirstOrCreate(&msfHost). 96 | Error 97 | if err != nil { 98 | return fmt.Errorf("query host %v: %w", preferredIP, err) 99 | } 100 | 101 | msfHost.WorkspaceId = workspaceId 102 | msfHost.Address = preferredIP.String() 103 | 104 | allMacs := nmapHost.AllMacAddresses() 105 | if len(allMacs) > 0 { 106 | msfHost.MAC = allMacs[0].String() 107 | } 108 | 109 | allHostnames := nmapHost.AllHostnames() 110 | if len(allHostnames) > 0 { 111 | msfHost.Name = allHostnames[0] 112 | } 113 | 114 | msfHost.State = "alive" 115 | 116 | if len(nmapHost.Os.Osmatch) > 0 { 117 | msfHost.OSName = nmapHost.Os.Osmatch[0].Name 118 | } 119 | 120 | if msfHost.Purpose == "" { 121 | msfHost.Purpose = "device" 122 | } 123 | 124 | if len(nmapHost.Os.Osclass) > 0 { 125 | msfHost.Purpose = nmapHost.Os.Osclass[0].Type 126 | } 127 | 128 | if msfHost.CreatedAt.IsZero() { 129 | msfHost.CreatedAt = now 130 | } 131 | 132 | msfHost.UpdatedAt = now 133 | 134 | err = tx.Save(&msfHost).Error 135 | if err != nil { 136 | return fmt.Errorf("save host %v: %w", msfHost, err) 137 | } 138 | 139 | log.Debugf("Inserted/updated host %s.", nmapHost) 140 | 141 | for _, port := range nmapHost.Ports.Port { 142 | err := InsertService(tx, msfHost.Id, port) 143 | if err != nil { 144 | return fmt.Errorf("insert port %s/%d for host %s: %w", port.Protocol, port.Portid, nmapHost, err) 145 | } 146 | 147 | openPortCount++ 148 | } 149 | 150 | return nil 151 | }) 152 | 153 | return openPortCount, nil 154 | } 155 | 156 | func InsertService(db *gorm.DB, hostId int, service NmapService) error { 157 | if service.State.State != "open" { 158 | return nil 159 | } 160 | 161 | var msfService MsfService 162 | 163 | err := db. 164 | Clauses(clause.Locking{Strength: "UPDATE"}). 165 | Where( 166 | "host_id = ? AND proto = ? AND port = ?", 167 | hostId, 168 | service.Protocol, 169 | service.Portid, 170 | ). 171 | FirstOrCreate(&msfService). 172 | Error 173 | 174 | if err != nil { 175 | return fmt.Errorf("query service %v: %w", service, err) 176 | } 177 | 178 | now := time.Now() 179 | 180 | msfService.HostId = hostId 181 | msfService.Proto = service.Protocol 182 | msfService.Port = service.Portid 183 | msfService.State = service.State.State 184 | 185 | name := service.Service.Name 186 | if service.Service.Tunnel != "" { 187 | name = fmt.Sprintf("%s/%s", service.Service.Tunnel, service.Service.Name) 188 | } 189 | if name != "" { 190 | msfService.Name = name 191 | } 192 | 193 | if service.Service.Product != "" { 194 | msfService.Info = service.Service.Product 195 | 196 | if service.Service.Version != "" { 197 | msfService.Info = fmt.Sprintf("%s %s", msfService.Info, service.Service.Version) 198 | } 199 | } 200 | 201 | if msfService.CreatedAt.IsZero() { 202 | msfService.CreatedAt = now 203 | } 204 | 205 | msfService.UpdatedAt = now 206 | 207 | err = db.Save(&msfService).Error 208 | if err != nil { 209 | return fmt.Errorf("save service %v: %w", msfService, err) 210 | } 211 | 212 | log.Debugf("Inserted/updated service %s.", service) 213 | 214 | return nil 215 | } 216 | -------------------------------------------------------------------------------- /internal/nmap_xml.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "net" 7 | ) 8 | 9 | // main struct Nmaprun generated with "XML to Go" (https://www.onlinetool.io/xmltogo/) 10 | // based on: https://github.com/nmap/nmap/blob/972ed6bac0951dbf6fac7e13550f6a429b316e4e/zenmap/radialnet/share/sample/nmap_example.xml 11 | 12 | type NmaprunHeader struct { 13 | XMLName xml.Name `xml:"nmaprun"` 14 | Text string `xml:",chardata"` 15 | Scanner string `xml:"scanner,attr"` 16 | Args string `xml:"args,attr"` 17 | Start string `xml:"start,attr"` 18 | Startstr string `xml:"startstr,attr"` 19 | Version string `xml:"version,attr"` 20 | Xmloutputversion string `xml:"xmloutputversion,attr"` 21 | } 22 | 23 | type NmapService struct { 24 | Text string `xml:",chardata"` 25 | Protocol string `xml:"protocol,attr"` 26 | Portid int `xml:"portid,attr"` 27 | State struct { 28 | Text string `xml:",chardata"` 29 | State string `xml:"state,attr"` 30 | Reason string `xml:"reason,attr"` 31 | ReasonTtl string `xml:"reason_ttl,attr"` 32 | } `xml:"state"` 33 | Service struct { 34 | Text string `xml:",chardata"` 35 | Name string `xml:"name,attr"` 36 | Product string `xml:"product,attr"` 37 | Version string `xml:"version,attr"` 38 | Extrainfo string `xml:"extrainfo,attr"` 39 | Method string `xml:"method,attr"` 40 | Conf string `xml:"conf,attr"` 41 | Tunnel string `xml:"tunnel,attr"` 42 | Hostname string `xml:"hostname,attr"` 43 | Servicefp string `xml:"servicefp,attr"` 44 | Ostype string `xml:"ostype,attr"` 45 | } `xml:"service"` 46 | Script []struct { 47 | Text string `xml:",chardata"` 48 | ID string `xml:"id,attr"` 49 | Output string `xml:"output,attr"` 50 | } `xml:"script"` 51 | } 52 | 53 | type NmapHost struct { 54 | Text string `xml:",chardata"` 55 | Status struct { 56 | Text string `xml:",chardata"` 57 | State string `xml:"state,attr"` 58 | Reason string `xml:"reason,attr"` 59 | } `xml:"status"` 60 | Address []struct { 61 | Text string `xml:",chardata"` 62 | Addr string `xml:"addr,attr"` 63 | Addrtype string `xml:"addrtype,attr"` 64 | } `xml:"address"` 65 | Hostnames struct { 66 | Text string `xml:",chardata"` 67 | Hostname []struct { 68 | Text string `xml:",chardata"` 69 | Name string `xml:"name,attr"` 70 | Type string `xml:"type,attr"` 71 | } `xml:"hostname"` 72 | } `xml:"hostnames"` 73 | Ports struct { 74 | Text string `xml:",chardata"` 75 | Extraports []struct { 76 | Text string `xml:",chardata"` 77 | State string `xml:"state,attr"` 78 | Count string `xml:"count,attr"` 79 | Extrareasons []struct { 80 | Text string `xml:",chardata"` 81 | Reason string `xml:"reason,attr"` 82 | Count string `xml:"count,attr"` 83 | } `xml:"extrareasons"` 84 | } `xml:"extraports"` 85 | Port []NmapService `xml:"port"` 86 | } `xml:"ports"` 87 | Os struct { 88 | Text string `xml:",chardata"` 89 | Portused []struct { 90 | Text string `xml:",chardata"` 91 | State string `xml:"state,attr"` 92 | Proto string `xml:"proto,attr"` 93 | Portid string `xml:"portid,attr"` 94 | } `xml:"portused"` 95 | Osclass []struct { 96 | Text string `xml:",chardata"` 97 | Type string `xml:"type,attr"` 98 | Vendor string `xml:"vendor,attr"` 99 | Osfamily string `xml:"osfamily,attr"` 100 | Osgen string `xml:"osgen,attr"` 101 | Accuracy string `xml:"accuracy,attr"` 102 | } `xml:"osclass"` 103 | Osmatch []struct { 104 | Text string `xml:",chardata"` 105 | Name string `xml:"name,attr"` 106 | Accuracy string `xml:"accuracy,attr"` 107 | Line string `xml:"line,attr"` 108 | } `xml:"osmatch"` 109 | Osfingerprint struct { 110 | Text string `xml:",chardata"` 111 | Fingerprint string `xml:"fingerprint,attr"` 112 | } `xml:"osfingerprint"` 113 | } `xml:"os"` 114 | Uptime struct { 115 | Text string `xml:",chardata"` 116 | Seconds string `xml:"seconds,attr"` 117 | Lastboot string `xml:"lastboot,attr"` 118 | } `xml:"uptime"` 119 | Tcpsequence struct { 120 | Text string `xml:",chardata"` 121 | Index string `xml:"index,attr"` 122 | Class string `xml:"class,attr"` 123 | Difficulty string `xml:"difficulty,attr"` 124 | Values string `xml:"values,attr"` 125 | } `xml:"tcpsequence"` 126 | Ipidsequence struct { 127 | Text string `xml:",chardata"` 128 | Class string `xml:"class,attr"` 129 | Values string `xml:"values,attr"` 130 | } `xml:"ipidsequence"` 131 | Tcptssequence struct { 132 | Text string `xml:",chardata"` 133 | Class string `xml:"class,attr"` 134 | Values string `xml:"values,attr"` 135 | } `xml:"tcptssequence"` 136 | Trace struct { 137 | Text string `xml:",chardata"` 138 | Port string `xml:"port,attr"` 139 | Proto string `xml:"proto,attr"` 140 | Hop []struct { 141 | Text string `xml:",chardata"` 142 | Ttl string `xml:"ttl,attr"` 143 | Rtt string `xml:"rtt,attr"` 144 | Ipaddr string `xml:"ipaddr,attr"` 145 | Host string `xml:"host,attr"` 146 | } `xml:"hop"` 147 | } `xml:"trace"` 148 | Times struct { 149 | Text string `xml:",chardata"` 150 | Srtt string `xml:"srtt,attr"` 151 | Rttvar string `xml:"rttvar,attr"` 152 | To string `xml:"to,attr"` 153 | } `xml:"times"` 154 | Distance struct { 155 | Text string `xml:",chardata"` 156 | Value string `xml:"value,attr"` 157 | } `xml:"distance"` 158 | } 159 | 160 | type Nmaprun struct { 161 | XMLName xml.Name `xml:"nmaprun"` 162 | Text string `xml:",chardata"` 163 | Scanner string `xml:"scanner,attr"` 164 | Args string `xml:"args,attr"` 165 | Start string `xml:"start,attr"` 166 | Startstr string `xml:"startstr,attr"` 167 | Version string `xml:"version,attr"` 168 | Xmloutputversion string `xml:"xmloutputversion,attr"` 169 | Scaninfo struct { 170 | Text string `xml:",chardata"` 171 | Type string `xml:"type,attr"` 172 | Protocol string `xml:"protocol,attr"` 173 | Numservices string `xml:"numservices,attr"` 174 | Services string `xml:"services,attr"` 175 | } `xml:"scaninfo"` 176 | Verbose struct { 177 | Text string `xml:",chardata"` 178 | Level string `xml:"level,attr"` 179 | } `xml:"verbose"` 180 | Debugging struct { 181 | Text string `xml:",chardata"` 182 | Level string `xml:"level,attr"` 183 | } `xml:"debugging"` 184 | Taskbegin []struct { 185 | Text string `xml:",chardata"` 186 | Task string `xml:"task,attr"` 187 | Time string `xml:"time,attr"` 188 | } `xml:"taskbegin"` 189 | Taskend []struct { 190 | Text string `xml:",chardata"` 191 | Task string `xml:"task,attr"` 192 | Time string `xml:"time,attr"` 193 | Extrainfo string `xml:"extrainfo,attr"` 194 | } `xml:"taskend"` 195 | Taskprogress []struct { 196 | Text string `xml:",chardata"` 197 | Task string `xml:"task,attr"` 198 | Time string `xml:"time,attr"` 199 | Percent string `xml:"percent,attr"` 200 | Remaining string `xml:"remaining,attr"` 201 | Etc string `xml:"etc,attr"` 202 | } `xml:"taskprogress"` 203 | Hosts []NmapHost `xml:"host"` 204 | Runstats struct { 205 | Text string `xml:",chardata"` 206 | Finished struct { 207 | Text string `xml:",chardata"` 208 | Time string `xml:"time,attr"` 209 | Timestr string `xml:"timestr,attr"` 210 | } `xml:"finished"` 211 | Hosts struct { 212 | Text string `xml:",chardata"` 213 | Up string `xml:"up,attr"` 214 | Down string `xml:"down,attr"` 215 | Total string `xml:"total,attr"` 216 | } `xml:"hosts"` 217 | } `xml:"runstats"` 218 | } 219 | 220 | func (h NmapHost) HasOpenPorts() bool { 221 | for _, port := range h.Ports.Port { 222 | if port.State.State == "open" { 223 | return true 224 | } 225 | } 226 | return false 227 | } 228 | 229 | func (h NmapHost) AllIPAddresses() []net.IP { 230 | addresses := make([]net.IP, 0) 231 | 232 | for _, addr := range h.Address { 233 | if addr.Addrtype == "ipv4" || addr.Addrtype == "ipv6" { 234 | parsed := net.ParseIP(addr.Addr) 235 | if parsed == nil { 236 | continue 237 | } 238 | 239 | addresses = append(addresses, parsed) 240 | } 241 | } 242 | 243 | return addresses 244 | } 245 | 246 | func (h NmapHost) AllHostnames() []string { 247 | hostnames := make([]string, 0) 248 | 249 | for _, hostname := range h.Hostnames.Hostname { 250 | hostnames = append(hostnames, hostname.Name) 251 | } 252 | 253 | return hostnames 254 | } 255 | 256 | func (h NmapHost) AllMacAddresses() []net.HardwareAddr { 257 | addresses := make([]net.HardwareAddr, 0) 258 | 259 | for _, addr := range h.Address { 260 | if addr.Addrtype == "mac" { 261 | parsed, err := net.ParseMAC(addr.Addr) 262 | if err != nil { 263 | continue 264 | } 265 | 266 | addresses = append(addresses, parsed) 267 | } 268 | } 269 | 270 | return addresses 271 | } 272 | 273 | func (h NmapHost) String() string { 274 | ips := h.AllIPAddresses() 275 | if len(ips) > 0 { 276 | return ips[0].String() 277 | } 278 | 279 | hostnames := h.AllHostnames() 280 | if len(hostnames) > 0 { 281 | return hostnames[0] 282 | } 283 | 284 | return "" 285 | } 286 | 287 | func (s NmapService) NameWithTunnel() string { 288 | prefix := "" 289 | if s.Service.Tunnel != "" { 290 | prefix = fmt.Sprintf("%s/", s.Service.Tunnel) 291 | } 292 | return fmt.Sprintf("%s%s", prefix, s.Service.Name) 293 | } 294 | 295 | func (s NmapService) String() string { 296 | suffix := "" 297 | 298 | if s.Service.Name != "" { 299 | suffix = fmt.Sprintf(" (%s)", s.NameWithTunnel()) 300 | } 301 | 302 | return fmt.Sprintf("%s:%d%s", s.Protocol, s.Portid, suffix) 303 | } 304 | -------------------------------------------------------------------------------- /internal/testdata/localhost.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | cpe:/a:postgresql:postgresql 19 | 20 | 21 | 22 | 23 | 24 | cpe:/o:linux:linux_kernel:2.6.32 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /internal/testdata/scanme.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | cpe:/a:openbsd:openssh:6.6.1p1 31 | cpe:/o:linux:linux_kernel 32 | 33 | 34 | 35 | 36 | 37 | cpe:/a:apache:http_server:2.4.7 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | --------------------------------------------------------------------------------