Codebase list balboa / 5396503
New upstream version 1.0 Sascha Steinbiss 5 years ago
48 changed file(s) with 5682 addition(s) and 0 deletion(s). Raw diff Collapse all Expand all
0 # Golang CircleCI 2.0 configuration file
1 #
2 # Check https://circleci.com/docs/2.0/language-go/ for more details
3 version: 2
4 jobs:
5 build:
6 docker:
7 - image: circleci/golang:1.10-stretch
8
9 working_directory: /go/src/github.com/DCSO/balboa
10 steps:
11 - checkout
12 - run:
13 name: Add stretch-backports
14 command: 'echo "deb http://ftp.debian.org/debian stretch-backports main" | sudo tee -a /etc/apt/sources.list.d/backports.list'
15 - run:
16 name: Install apt dependencies
17 command: 'sudo apt-get update && sudo apt-get -t stretch-backports install librocksdb-dev libtpl-dev -y'
18
19 # specify any bash command here prefixed with `run: `
20 - run: go get -v -t -d ./...
21 - run: go test -v ./...
0 language: go
1 sudo: false
2 script:
3 - go vet ./...
4 - go test -v ./...
0 Copyright (c) 2018, DCSO Deutsche Cyber-Sicherheitsorganisation GmbH
1 All rights reserved.
2
3 Redistribution and use in source and binary forms, with or without
4 modification, are permitted provided that the following conditions are met:
5
6 * Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
8
9 * Redistributions in binary form must reproduce the above copyright notice,
10 this list of conditions and the following disclaimer in the documentation
11 and/or other materials provided with the distribution.
12
13 * Neither the name of the DCSO Deutsche Cyber-Sicherheitsorganisation GmbH
14 nor the names of its contributors may be used to endorse or promote products
15 derived from this software without specific prior written permission.
16
17 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0 # balboa [![CircleCI](https://circleci.com/gh/DCSO/balboa.svg?style=svg)](https://circleci.com/gh/DCSO/balboa)
1
2 balboa is the BAsic Little Book Of Answers. It consumes and indexes observations from [passive DNS](https://www.farsightsecurity.com/technical/passive-dns/) collection, providing a [GraphQL](https://graphql.org/) interface to access the aggregated contents of the observations database. We built balboa to handle passive DNS data aggregated from metadata gathered by [Suricata](https://suricata-ids.org).
3
4 The API should be suitable for integration into existing multi-source observable integration frameworks. It is possible to produce results in a [Common Output Format](https://datatracker.ietf.org/doc/draft-dulaunoy-dnsop-passive-dns-cof/) compatible schema using the GraphQL API. In fact, the GraphQL schema is modelled after the COF fields.
5
6 The balboa software...
7
8 - is fast for queries and input/updates
9 - implements persistent, compressed storage of observations
10 - as local storage
11 - in a [Cassandra](https://cassandra.apache.org) cluster (experimental)
12 - supports tracking and specifically querying multiple sensors
13 - makes use of multi-core systems
14 - can accept input from multiple sources simultaneously
15 - HTTP (POST)
16 - AMQP
17 - GraphQL
18 - Unix socket
19 - accepts various (text-based) input formats
20 - JSON-based
21 - [FEVER](https://github.com/DCSO/fever)
22 - [gopassivedns](https://github.com/Phillipmartin/gopassivedns)
23 - [Packetbeat](https://www.elastic.co/guide/en/beats/packetbeat/master/packetbeat-dns-options.html) (via Logstash)
24 - [Suricata EVE DNS v1 and v2](http://suricata.readthedocs.io/en/latest/output/eve/eve-json-format.html#event-type-dns)
25 - flat file
26 - Edward Fjellskål's [PassiveDNS](https://github.com/gamelinux/passivedns) tabular format (default order `-f SMcsCQTAtn`)
27
28 ## Building and Installation
29
30 ```
31 $ go get github.com/DCSO/balboa
32 ```
33
34 Dependencies:
35
36 - Go 1.7 or later
37 - [RocksDB](https://rocksdb.org/) 5.0 or later (shared lib, with LZ4 support)
38 - [tpl](https://troydhanson.github.io/tpl/index.html) (shared lib)
39
40 On Debian (testing and stretch-backports), one can satisfy these dependencies with:
41
42 ```
43 apt install librocksdb-dev libtpl-dev
44 ```
45
46 ## Usage
47
48 ### Configuring feeders
49
50 Feeders are used to get observations into the database. They run concurrently and process inputs in the background, making results accessible via the query interface as soon as the resulting upsert transactions have been completed in the database. What feeders are to be created is defined in a YAML configuration file (to be passed via the `-f` parameter to `balboa serve`). Example:
51
52 ```yaml
53 feeder:
54 - name: AMQP Input
55 type: amqp
56 url: amqp://guest:guest@localhost:5672
57 exchange: [ tdh.pdns ]
58 input_format: fever_aggregate
59 - name: HTTP Input
60 type: http
61 listen_host: 127.0.0.1
62 listen_port: 8081
63 input_format: fever_aggregate
64 - name: Socket Input
65 type: socket
66 path: /tmp/balboa.sock
67 input_format: gopassivedns
68 ```
69
70 A balboa instance given this feeder configuration would support the following input options:
71
72 - JSON in FEVER's aggregate format delivered via AMQP from a temporary queue attached to the exchange `tdh.pdns` on `localhost` port 5762, authenticated with user `guest` and password `guest`
73 - JSON in FEVER's aggregate format parsed from HTTP POST requests on port 8081 on the local system
74 - JSON in gopassivedns's format, fed into the UNIX socket `/tmp/balboa.sock` created by balboa
75
76 All of these feeders accept input simultaneously, there is no distinction made as to where an observation has come from. It is possible to specify multiple feeders of the same type but with different settings as long as their `name`s are unique.
77
78 ### Configuring the database backend
79
80 Multiple database backends are supported to store pDNS observations persistently. The one to use in a particular instance can be configured separately in another YAML file (to be passed via the `-d` parameter to `balboa serve`). For example, to use the RocksDB backend storing data in `/tmp/balboa`, one would specify:
81
82 ```yaml
83 database:
84 name: Local RocksDB
85 type: rocksdb
86 db_path: /tmp/balboa
87 ```
88
89 To alternatively, for instance, send all data to a Cassandra cluster, use the following configuration:
90
91 ```yaml
92 database:
93 name: Cassandra cluster
94 type: cassandra
95 hosts: [ "127.0.0.1", "127.0.0.2", "127.0.0.3" ]
96 username: cassandra
97 password: cassandra
98 ```
99
100 which would use the specified nodes to access the cluster. Only one database can be configured at a time.
101
102 ### Running the server and consuming input
103
104 All interaction with the service on the command line takes place via the `balboa` executable. The server can be started using:
105
106 ```
107 $ balboa serve -l ''
108 INFO[0000] Local RocksDB: memory budget empty, using default of 128MB
109 INFO[0000] starting database Local RocksDB
110 INFO[0000] opening database...
111 INFO[0000] database ready
112 INFO[0000] starting feeder AMQPInput2
113 INFO[0000] starting feeder HTTP Input
114 INFO[0000] accepting submissions on port 8081
115 INFO[0000] starting feeder Socket Input
116 INFO[0000] starting feeder Suricata Socket Input
117 INFO[0000] serving GraphQL on port 8080
118 ```
119
120 Depending on the size of the existing database, the start-up time ("opening database...") might be rather long if there is a lot of log to go through.
121
122 After startup, the feeders are free to be used for data ingest. For example, one might do some of the following to test data consumption (assuming the feeders above are used):
123
124 - for AMQP:
125
126 ```
127 $ scripts/mkjson.py | rabbitmqadmin publish routing_key="" exchange=tdh.pdns
128 ```
129
130 - for HTTP:
131 ```
132 $ scripts/mkjson.py | curl -d@- -qs --header "X-Sensor-ID: abcde" http://localhost:8081/submit
133 ```
134
135 - for socket:
136 ```
137 $ sudo gopassivedns -dev eth0 | socat /tmp/balboa.sock STDIN
138 ```
139
140 Besides these asynchronous updates, it is always possible to use the `announceObservation` mutation in the GraphQL interface to explicitly add an observation, returning the updated data entry immediately:
141
142 ```graphql
143 mutation {
144 announceObservation(observation: {
145 rrname: "test.foobar.de",
146 rrtype: A,
147 rdata: "1.2.3.4",
148 time_first: 1531943211,
149 time_last: 1531949570,
150 sensor_id: "abcde",
151 count: 3
152 }) {
153 count
154 }
155 }
156 ```
157
158 This request would synchronously add a new observation with the given input data to the database and then return the new, updated `count` value.
159
160 ### Querying the server
161
162 The intended main interface for interacting with the server is via GraphQL. For example, the query
163
164 ```graphql
165 query {
166 entries(rrname: "test.foobar.de", sensor_id: "abcde") {
167 rrname
168 rrtype
169 rdata
170 time_first
171 time_last
172 sensor_id
173 count
174 }
175 }
176 ```
177
178 would return something like
179
180 ```json
181 {
182 "data": {
183 "entries": [
184 {
185 "rrname": "test.foobar.de",
186 "rrtype": "A",
187 "rdata": "1.2.3.4",
188 "time_first": 1531943211,
189 "time_last": 1531949570,
190 "sensor_id": "abcde",
191 "count": 3
192 }
193 ]
194 }
195 }
196 ```
197
198 This also works with `rdata` as the query parameter, but at least one of `rrname` or `rdata` must be stated. If there is no `sensor_id` parameter, then all results will be returned regardless of where the DNS answer was observed. Use the `time_first_rfc3339` and `time_last_rfc3339` instead of `time_first` and `time_last`, respectively, to get human-readable timestamps.
199
200 ### Aliases
201
202 Sometimes it is interesting to ask for all the domain names that resolve to the same IP address. For this reason, the GraphQL API supports a virtual `aliases` field that returns all Entries with RRType `A` or `AAAA` that share the same address in the Rdata field.
203
204 Example:
205
206 ```graphql
207 {
208 entries(rrname: "heise.de", rrtype: A) {
209 rrname
210 rdata
211 rrtype
212 time_first_rfc3339
213 time_last_rfc3339
214 aliases {
215 rrname
216 time_first_rfc3339
217 time_last_rfc3339
218 }
219 }
220 }
221 ```
222
223 ```json
224 {
225 "data": {
226 "entries": [
227 {
228 "rrname": "heise.de",
229 "rdata": "193.99.144.80",
230 "rrtype": "A",
231 "time_first_rfc3339": "2018-07-10T08:05:45Z",
232 "time_last_rfc3339": "2018-10-18T09:24:38Z",
233 "aliases": [
234 {
235 "rrname": "ct.de"
236 },
237 {
238 "rrname": "ix.de"
239 },
240 {
241 "rrname": "redirector.heise.de"
242 },
243 {
244 "rrname": "www.ix.de"
245 }
246 ]
247 }
248 ]
249 }
250 }
251 ```
252
253 ### Bulk queries
254
255 There is also a shortcut tool to make 'bulk' querying easier. For example, to get all the information on the hosts in range 1.2.0.0/16 as observed by sensor `abcde`, one can use:
256
257 ```
258 $ balboa query --sensor abcde 1.2.0.0/16
259 {"count":6,"time_first":1531943211,"time_last":1531949570,"rrtype":"A","rrname":"test.foobar.de","rdata":"1.2.3.4","sensor_id":"abcde"}
260 {"count":1,"time_first":1531943215,"time_last":1531949530,"rrtype":"A","rrname":"baz.foobar.de","rdata":"1.2.3.7","sensor_id":"abcde"}
261 ```
262 Note that this tool currently only does a lot of concurrent individual queries! To improve performance in these cases it might be worthwhile to allow for range queries on the server side as well in the future.
263
264 ### Other tools
265
266 Run `balboa` without arguments to list available subcommands and get a short description of what they do.
267
268 ## Author/Contact
269
270 Sascha Steinbiss
271
272 ## License
273
274 BSD-3-clause
0 [Unit]
1 Description=Basic Little Book of Answers
2 Documentation=https://github.com/DCSO/balboa
3 After=network.target
4
5 [Service]
6 SyslogIdentifier=balboa
7 EnvironmentFile=-/etc/default/balboa
8 ExecStart=/usr/bin/balboa serve $BALBOA_ARGS
9 ExecStop=/usr/bin/pkill balboa
10 PIDFile=/var/run/balboa/balboa.pid
11 LimitNOFILE=200000
12 Restart=on-failure
13 RestartSec=5
14
15 [Install]
16 WantedBy=multi-user.target
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package cmds
4
5 import (
6 "context"
7 "fmt"
8 "math/rand"
9 "sync"
10 "time"
11
12 "github.com/machinebox/graphql"
13 log "github.com/sirupsen/logrus"
14 "github.com/spf13/cobra"
15 )
16
17 // benchCmd represents the bench command
18 var benchCmd = &cobra.Command{
19 Use: "bench",
20 Short: "Run a number of queries against given endpoint",
21 Long: `This command issues a potentially large number of queries against a
22 given GraphQL endpoint, for example for benchmarking. It does this in a concurrent
23 fashion.`,
24 Run: func(cmd *cobra.Command, args []string) {
25 var err error
26 var wg sync.WaitGroup
27 rand.Seed(time.Now().Unix())
28
29 var verbose bool
30 verbose, err = cmd.Flags().GetBool("verbose")
31 if err != nil {
32 log.Fatal(err)
33 }
34 if verbose {
35 log.SetLevel(log.DebugLevel)
36 }
37
38 var url string
39 url, err = cmd.Flags().GetString("url")
40 if err != nil {
41 log.Fatal(err)
42 }
43 client := graphql.NewClient(url)
44
45 var count int
46 count, err = cmd.Flags().GetInt("count")
47 if err != nil {
48 log.Fatal(err)
49 }
50
51 ipChan := make(chan string, count)
52 for i := 0; i < 5; i++ {
53 wg.Add(1)
54 go func(mywg *sync.WaitGroup) {
55 defer mywg.Done()
56 req := graphql.NewRequest(`
57 query ($ip: String!){
58 entries(rdata: $ip) {
59 rdata
60 rrname
61 rrtype
62 time_first
63 time_last
64 count
65 sensor_id
66 }
67 }`)
68 req.Header.Set("Cache-Control", "no-cache")
69 for ip := range ipChan {
70 req.Var("ip", ip)
71 var respData observationResultSet
72 if err := client.Run(context.Background(), req, &respData); err != nil {
73 log.Fatal(err)
74 }
75 if len(respData.Entries) > 0 {
76 log.Info(respData)
77 }
78 }
79 }(&wg)
80 }
81
82 for i := int(0); i < count; i++ {
83 if i%1000 == 0 {
84 log.Info(i)
85 }
86 ip := fmt.Sprintf("%d.%d.%d.%d", rand.Intn(255), rand.Intn(255),
87 rand.Intn(255), rand.Intn(255))
88 ipChan <- ip
89 }
90 close(ipChan)
91 wg.Wait()
92 },
93 }
94
95 func init() {
96 rootCmd.AddCommand(benchCmd)
97
98 benchCmd.Flags().BoolP("verbose", "v", false, "verbose mode")
99 benchCmd.Flags().StringP("url", "u", "http://localhost:8080/query", "URL of GraphQL interface to query")
100 benchCmd.Flags().IntP("count", "c", 10000, "number of queries")
101 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package cmds
4
5 import (
6 log "github.com/sirupsen/logrus"
7 "github.com/spf13/cobra"
8 "github.com/spf13/cobra/doc"
9 )
10
11 // mmanCmd represents the makeman command
12 var mmanCmd = &cobra.Command{
13 Use: "makeman [options]",
14 Short: "Create man pages",
15 Run: func(cmd *cobra.Command, args []string) {
16 targetDir, err := cmd.Flags().GetString("dir")
17 if err != nil {
18 log.Fatal(err)
19 }
20 header := &doc.GenManHeader{}
21 err = doc.GenManTree(rootCmd, header, targetDir)
22 if err != nil {
23 log.Fatal(err)
24 }
25 for _, v := range rootCmd.Commands() {
26 err = doc.GenManTree(v, header, targetDir)
27 if err != nil {
28 log.Fatal(err)
29 }
30 }
31 },
32 }
33
34 func init() {
35 rootCmd.AddCommand(mmanCmd)
36 mmanCmd.Flags().StringP("dir", "d", ".", "target directory for man pages")
37 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package cmds
4
5 import (
6 "context"
7 "encoding/json"
8 "fmt"
9 "net"
10 "sync"
11
12 "github.com/machinebox/graphql"
13 log "github.com/sirupsen/logrus"
14 "github.com/spf13/cobra"
15 )
16
17 type observationResultSet struct {
18 Entries []observationResult
19 }
20
21 type observationResult struct {
22 Count int `json:"count"`
23 TimeFirst int `json:"time_first"`
24 TimeLast int `json:"time_last"`
25 RRType string `json:"rrtype"`
26 RRName string `json:"rrname"`
27 RData string `json:"rdata"`
28 SensorID string `json:"sensor_id"`
29 }
30
31 func incIP(ip net.IP) {
32 for j := len(ip) - 1; j >= 0; j-- {
33 ip[j]++
34 if ip[j] > 0 {
35 break
36 }
37 }
38 }
39
40 func hosts(cidr string) ([]string, error) {
41 ip, ipnet, err := net.ParseCIDR(cidr)
42 if err != nil {
43 return nil, err
44 }
45
46 var ips []string
47 for ip := ip.Mask(ipnet.Mask); ipnet.Contains(ip); incIP(ip) {
48 ips = append(ips, ip.String())
49 }
50
51 // remove network address and broadcast address
52 if len(ips) > 2 {
53 return ips[1 : len(ips)-1], nil
54 }
55 return ips, nil
56 }
57
58 var queryCmd = &cobra.Command{
59 Use: "query [netmask]",
60 Short: "Obtain information from pDNS about IP ranges",
61 Long: `This command allows to query a balboa endpoint for information regarding
62 IPs from a given range.`,
63 Run: func(cmd *cobra.Command, args []string) {
64 var err error
65
66 if len(args) == 0 {
67 log.Fatal("needs network as argument, e.g. '192.168.0.0/24'")
68 }
69
70 var verbose bool
71 verbose, err = cmd.Flags().GetBool("verbose")
72 if err != nil {
73 log.Fatal(err)
74 }
75 if verbose {
76 log.SetLevel(log.DebugLevel)
77 }
78
79 var url string
80 url, err = cmd.Flags().GetString("url")
81 if err != nil {
82 log.Fatal(err)
83 }
84 client := graphql.NewClient(url)
85
86 var senid string
87 senid, err = cmd.Flags().GetString("sensor")
88 if err != nil {
89 log.Fatal(err)
90 }
91
92 var wg sync.WaitGroup
93 ipChan := make(chan string, 1000)
94 for i := 0; i < 5; i++ {
95 wg.Add(1)
96 go func(mywg *sync.WaitGroup) {
97 defer mywg.Done()
98 req := graphql.NewRequest(`
99 query ($ip: String!, $senid: String){
100 entries(rdata: $ip, sensor_id: $senid) {
101 rdata
102 rrname
103 rrtype
104 time_first
105 time_last
106 count
107 sensor_id
108 }
109 }`)
110 req.Header.Set("Cache-Control", "no-cache")
111 for ip := range ipChan {
112 req.Var("ip", ip)
113 if senid != "" {
114 req.Var("senid", senid)
115 }
116 var respData observationResultSet
117 if err := client.Run(context.Background(), req, &respData); err != nil {
118 log.Fatal(err)
119 }
120 if len(respData.Entries) > 0 {
121 for _, entry := range respData.Entries {
122 out, err := json.Marshal(entry)
123 if err != nil {
124 log.Fatal(err)
125 }
126 fmt.Println(string(out))
127 }
128 }
129 }
130 }(&wg)
131 }
132
133 hosts, err := hosts(args[0])
134 if err != nil {
135 log.Fatal(err)
136 }
137 for _, host := range hosts {
138 ipChan <- host
139 }
140 close(ipChan)
141 wg.Wait()
142 },
143 }
144
145 func init() {
146 rootCmd.AddCommand(queryCmd)
147
148 queryCmd.Flags().BoolP("verbose", "v", false, "verbose mode")
149 queryCmd.Flags().StringP("url", "u", "http://localhost:8080/query", "URL of GraphQL interface to query")
150 queryCmd.Flags().StringP("sensor", "s", "", "limit query to observations from single sensor")
151 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package cmds
4
5 import (
6 "github.com/DCSO/balboa/db"
7 log "github.com/sirupsen/logrus"
8 "github.com/spf13/cobra"
9 )
10
11 var rocksdumpCmd = &cobra.Command{
12 Use: "rocksdump [netmask]",
13 Short: "Dump information from RocksDB",
14 Long: `This command allows directly dump bulk information from RocksDB.`,
15 Run: func(cmd *cobra.Command, args []string) {
16 var err error
17
18 if len(args) == 0 {
19 log.Fatal("needs database path as argument")
20 }
21
22 var verbose bool
23 verbose, err = cmd.Flags().GetBool("verbose")
24 if err != nil {
25 log.Fatal(err)
26 }
27 if verbose {
28 log.SetLevel(log.DebugLevel)
29 }
30
31 rdb, err := db.MakeRocksDBReadonly(args[0])
32 if err != nil {
33 log.Fatal(err)
34 }
35
36 rdb.Dump()
37 },
38 }
39
40 func init() {
41 rootCmd.AddCommand(rocksdumpCmd)
42
43 rocksdumpCmd.Flags().BoolP("verbose", "v", false, "verbose mode")
44 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package cmds
4
5 import (
6 "fmt"
7 "os"
8 "os/user"
9
10 "github.com/spf13/cobra"
11 "github.com/spf13/viper"
12 )
13
14 // rootCmd represents the base command when called without any subcommands
15 var rootCmd = &cobra.Command{
16 Use: "balboa",
17 Short: "BAsic Little Book Of Answers",
18 }
19
20 // Execute runs the root command's Execute method as the Execute
21 // hook for the whole package.
22 func Execute() {
23 if err := rootCmd.Execute(); err != nil {
24 fmt.Println(err)
25 os.Exit(1)
26 }
27 }
28
29 func init() {
30 cobra.OnInitialize(initConfig)
31 }
32
33 func initConfig() {
34 usr, _ := user.Current()
35 homedir := usr.HomeDir
36
37 // Search config in home directory with name ".balboa" (without extension).
38 viper.AddConfigPath(homedir)
39 viper.SetConfigName(".balboa")
40
41 viper.AutomaticEnv() // read in environment variables that match
42
43 // If a config file is found, read it in.
44 if err := viper.ReadInConfig(); err == nil {
45 fmt.Println("Using config file:", viper.ConfigFileUsed())
46 }
47 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package cmds
4
5 import (
6 "io/ioutil"
7 "os"
8 "os/signal"
9 "syscall"
10
11 "github.com/DCSO/balboa/db"
12 "github.com/DCSO/balboa/feeder"
13 "github.com/DCSO/balboa/observation"
14 "github.com/DCSO/balboa/query"
15 log "github.com/sirupsen/logrus"
16 "github.com/spf13/cobra"
17 )
18
19 var serveCmd = &cobra.Command{
20 Use: "serve",
21 Short: "Run the balboa server",
22 Long: `This command starts the balboa server, accepting submissions and
23 answering queries.`,
24 Run: func(cmd *cobra.Command, args []string) {
25 var err error
26
27 // handle verbosity
28 var verbose bool
29 verbose, err = cmd.Flags().GetBool("verbose")
30 if err != nil {
31 log.Fatal(err)
32 }
33 if verbose {
34 log.SetLevel(log.DebugLevel)
35 }
36
37 // handle logfile preferences
38 var logfile string
39 logfile, err = cmd.Flags().GetString("logfile")
40 if err != nil {
41 log.Fatal(err)
42 }
43 if logfile != "" {
44 lf, err := os.OpenFile(logfile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
45 if err != nil {
46 log.Fatal(err)
47 }
48 log.Infof("switching to log file %s", logfile)
49 defer lf.Close()
50 log.SetFormatter(&log.TextFormatter{
51 DisableColors: true,
52 FullTimestamp: true,
53 })
54 log.SetOutput(lf)
55 var logjson bool
56 logjson, err = cmd.Flags().GetBool("logjson")
57 if err != nil {
58 log.Fatal(err)
59 }
60 if logjson {
61 log.SetFormatter(&log.JSONFormatter{})
62 }
63 }
64
65 // Set up database from config file
66 var dbFile string
67 dbFile, err = cmd.Flags().GetString("dbconfig")
68 if err != nil {
69 log.Fatal(err)
70 }
71 cfgYaml, err := ioutil.ReadFile(dbFile)
72 if err != nil {
73 log.Fatal(err)
74 }
75 dbsetup, err := db.LoadSetup(cfgYaml)
76 if err != nil {
77 log.Fatal(err)
78 }
79 db.ObservationDB, err = dbsetup.Run()
80 if err != nil {
81 log.Fatal(err)
82 }
83
84 // Set up feeders from config file
85 var feedersFile string
86 feedersFile, err = cmd.Flags().GetString("feeders")
87 if err != nil {
88 log.Fatal(err)
89 }
90 cfgYaml, err = ioutil.ReadFile(feedersFile)
91 if err != nil {
92 log.Fatal(err)
93 }
94 fsetup, err := feeder.LoadSetup(cfgYaml)
95 if err != nil {
96 log.Fatal(err)
97 }
98 err = fsetup.Run(observation.InChan)
99 if err != nil {
100 log.Fatal(err)
101 }
102
103 // Start processing submissions
104 go db.ObservationDB.ConsumeFeed(observation.InChan)
105
106 // start query server
107 var port int
108 port, err = cmd.Flags().GetInt("port")
109 if err != nil {
110 log.Fatal(err)
111 }
112 gql := query.GraphQLFrontend{}
113 gql.Run(int(port))
114
115 sigChan := make(chan os.Signal, 1)
116 done := make(chan bool, 1)
117 signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
118 go func() {
119 sig := <-sigChan
120 log.Infof("received '%v' signal, shutting down", sig)
121 stopChan := make(chan bool)
122 fsetup.Stop(stopChan)
123 <-stopChan
124 stopChan = make(chan bool)
125 dbsetup.Stop(stopChan)
126 <-stopChan
127 gql.Stop()
128 close(done)
129 }()
130 <-done
131 },
132 }
133
134 func init() {
135 rootCmd.AddCommand(serveCmd)
136
137 serveCmd.Flags().BoolP("verbose", "v", false, "verbose mode")
138 serveCmd.Flags().StringP("dbconfig", "d", "database.yaml", "database configuration file")
139 serveCmd.Flags().StringP("feeders", "f", "feeders.yaml", "feeders configuraion file")
140 serveCmd.Flags().IntP("port", "p", 8080, "port for GraphQL server")
141 serveCmd.Flags().StringP("logfile", "l", "/var/log/balboa.log", "log file path")
142 serveCmd.Flags().BoolP("logjson", "j", true, "output log file as JSON")
143 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package main
4
5 import cmd "github.com/DCSO/balboa/cmd/balboa/cmds"
6
7 func main() {
8 cmd.Execute()
9 }
0 database:
1 name: Local RocksDB
2 type: rocksdb
3 db_path: /tmp/balboa
4
5 #database:
6 # name: Local Cassandra
7 # type: cassandra
8 # hosts: [ "127.0.0.1", "127.0.0.2", "127.0.0.3" ]
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package db
4
5 import "github.com/DCSO/balboa/observation"
6
7 // ObservationDB is the common DB instance used for this balboa session.
8 var ObservationDB DB
9
10 // DB abstracts a database backend for observation storage.
11 type DB interface {
12 AddObservation(observation.InputObservation) observation.Observation
13 ConsumeFeed(chan observation.InputObservation)
14 Shutdown()
15 TotalCount() (int, error)
16 Search(*string, *string, *string, *string) ([]observation.Observation, error)
17 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package db
4
5 import (
6 "time"
7
8 "github.com/DCSO/balboa/observation"
9
10 "github.com/gocql/gocql"
11 log "github.com/sirupsen/logrus"
12 )
13
14 // CassandraDB is a DB implementation based on Apache Cassandra.
15 type CassandraDB struct {
16 Cluster *gocql.ClusterConfig
17 Session *gocql.Session
18 StopChan chan bool
19 Nworkers uint
20 }
21
22 // MakeCassandraDB returns a new CassandraDB instance connecting to the
23 // provided hosts.
24 func MakeCassandraDB(hosts []string, username, password string, nofWorkers uint) (*CassandraDB, error) {
25 cluster := gocql.NewCluster(hosts...)
26 cluster.Keyspace = "balboa"
27 cluster.ProtoVersion = 4
28 cluster.PoolConfig.HostSelectionPolicy = gocql.TokenAwareHostPolicy(gocql.RoundRobinHostPolicy())
29 cluster.RetryPolicy = &gocql.ExponentialBackoffRetryPolicy{NumRetries: 5}
30 cluster.Consistency = gocql.Two
31 if len(username) > 0 && len(password) > 0 {
32 cluster.Authenticator = gocql.PasswordAuthenticator{
33 Username: username,
34 Password: password,
35 }
36 }
37 gsession, err := cluster.CreateSession()
38 if err != nil {
39 return nil, err
40 }
41 //session := gockle.NewSession(gsession)
42 db := &CassandraDB{
43 Cluster: cluster,
44 Session: gsession,
45 StopChan: make(chan bool),
46 Nworkers: nofWorkers,
47 }
48 return db, nil
49 }
50
51 //func makeCassandraDBMock() (*CassandraDB, *gockle.SessionMock) {
52 // mock := &gockle.SessionMock{}
53 // db := &CassandraDB{
54 // //Session: mock,
55 // StopChan: make(chan bool),
56 // }
57 // return db, mock
58 //}
59
60 // AddObservation adds a single observation synchronously to the database.
61 func (db *CassandraDB) AddObservation(obs observation.InputObservation) observation.Observation {
62 //Not implemented
63 log.Warn("AddObservation() not yet implemented on Cassandra backend")
64 return observation.Observation{}
65 }
66
67 func (db *CassandraDB) runChunkWorker(inChan chan observation.InputObservation) {
68 rdataUpd := db.Session.Query(`UPDATE observations_by_rdata SET last_seen = ? where rdata = ? and rrname = ? and rrtype = ? and sensor_id = ?;`)
69 rrnameUpd := db.Session.Query(`UPDATE observations_by_rrname SET last_seen = ? where rrname = ? and rdata = ? and rrtype = ? and sensor_id = ?;`)
70 firstseenUpd := db.Session.Query(`INSERT INTO observations_firstseen (first_seen, rrname, rdata, rrtype, sensor_id) values (?, ?, ?, ?, ?) IF NOT EXISTS;`)
71 countsUpd := db.Session.Query(`UPDATE observations_counts SET count = count + ? where rdata = ? and rrname = ? and rrtype = ? and sensor_id = ?;`)
72 for obs := range inChan {
73 select {
74 case <-db.StopChan:
75 log.Info("database ingest terminated")
76 return
77 default:
78 if obs.Rdata == "" {
79 obs.Rdata = "-"
80 }
81 if err := rdataUpd.Bind(obs.TimestampEnd, obs.Rdata, obs.Rrname, obs.Rrtype, obs.SensorID).Exec(); err != nil {
82 log.Error(err)
83 continue
84 }
85 if err := rrnameUpd.Bind(obs.TimestampEnd, obs.Rrname, obs.Rdata, obs.Rrtype, obs.SensorID).Exec(); err != nil {
86 log.Error(err)
87 continue
88 }
89 if err := firstseenUpd.Bind(obs.TimestampStart, obs.Rrname, obs.Rdata, obs.Rrtype, obs.SensorID).Exec(); err != nil {
90 log.Error(err)
91 continue
92 }
93 if err := countsUpd.Bind(obs.Count, obs.Rdata, obs.Rrname, obs.Rrtype, obs.SensorID).Exec(); err != nil {
94 log.Error(err)
95 continue
96 }
97 }
98 }
99 }
100
101 // ConsumeFeed accepts observations from a channel and queues them for
102 // database insertion.
103 func (db *CassandraDB) ConsumeFeed(inChan chan observation.InputObservation) {
104 var w uint
105 sendChan := make(chan observation.InputObservation, 500)
106 log.Debugf("Firing up %d workers", db.Nworkers)
107 for w = 1; w <= db.Nworkers; w++ {
108 go db.runChunkWorker(sendChan)
109 }
110 for {
111 select {
112 case <-db.StopChan:
113 log.Info("database ingest terminated")
114 return
115 case obs := <-inChan:
116 sendChan <- obs
117 }
118 }
119 }
120
121 // Search returns a slice of observations matching one or more criteria such
122 // as rdata, rrname, rrtype or sensor ID.
123 func (db *CassandraDB) Search(qrdata, qrrname, qrrtype, qsensorID *string) ([]observation.Observation, error) {
124 outs := make([]observation.Observation, 0)
125 var getQueryString string
126 var rdataFirst, hasSecond bool
127 var getQuery *gocql.Query
128
129 // determine appropriate table and parameterisation for query
130 if qrdata != nil {
131 rdataFirst = true
132 if qrrname != nil {
133 hasSecond = true
134 getQueryString = "SELECT * FROM observations_by_rdata WHERE rdata = ? and rrtype = ?"
135 } else {
136 hasSecond = false
137 getQueryString = "SELECT * FROM observations_by_rdata WHERE rdata = ?"
138 }
139 } else {
140 rdataFirst = false
141 if qrdata != nil {
142 hasSecond = true
143 getQueryString = "SELECT * FROM observations_by_rrname WHERE rrname = ? and rdata = ?"
144 } else {
145 hasSecond = false
146 getQueryString = "SELECT * FROM observations_by_rrname WHERE rrname = ?"
147 }
148 }
149 log.Debug(getQueryString)
150 getQuery = db.Session.Query(getQueryString)
151 getQuery.Consistency(gocql.One)
152
153 // do parameterised search
154 if rdataFirst {
155 if hasSecond {
156 getQuery.Bind(*qrdata, *qrrname)
157 } else {
158 getQuery.Bind(*qrdata)
159 }
160 } else {
161 if hasSecond {
162 getQuery.Bind(*qrrname, *qrdata)
163 } else {
164 getQuery.Bind(*qrrname)
165 }
166 }
167
168 // retrieve hits for initial queries
169 iter := getQuery.Iter()
170 for {
171 row := make(map[string]interface{})
172 if !iter.MapScan(row) {
173 break
174 }
175
176 if rrnameV, ok := row["rrname"]; ok {
177 var rdata, rrname, rrtype, sensorID string
178 var lastSeen int
179 rrname = rrnameV.(string)
180 if rdataV, ok := row["rdata"]; ok {
181 rdata = rdataV.(string)
182
183 // secondary filtering by sensor ID and RRType
184 if sensorIDV, ok := row["sensor_id"]; ok {
185 sensorID = sensorIDV.(string)
186 if qsensorID != nil && *qsensorID != sensorID {
187 continue
188 }
189 }
190 if rrtypeV, ok := row["rrtype"]; ok {
191 rrtype = rrtypeV.(string)
192 if qrrtype != nil && *qrrtype != rrtype {
193 continue
194 }
195 }
196 if lastSeenV, ok := row["last_seen"]; ok {
197 lastSeen = int(lastSeenV.(time.Time).Unix())
198 }
199
200 // we now have a result item
201 out := observation.Observation{
202 RRName: rrname,
203 RData: rdata,
204 RRType: rrtype,
205 SensorID: sensorID,
206 LastSeen: lastSeen,
207 }
208
209 // manual joins -> get additional data from counts table
210 tmpMap := make(map[string]interface{})
211 getCounters := db.Session.Query(`SELECT count FROM observations_counts WHERE rrname = ? AND rdata = ? AND rrtype = ? AND sensor_id = ?`).Bind(rrname, rdata, rrtype, sensorID)
212 err := getCounters.MapScan(tmpMap)
213 if err != nil {
214 log.Errorf("getCount: %s", err.Error())
215 continue
216 }
217 out.Count = int(tmpMap["count"].(int64))
218
219 tmpMap = make(map[string]interface{})
220 getFirstSeen := db.Session.Query(`SELECT first_seen FROM observations_firstseen WHERE rrname = ? AND rdata = ? AND rrtype = ? AND sensor_id = ?`).Bind(rrname, rdata, rrtype, sensorID)
221 err = getFirstSeen.MapScan(tmpMap)
222 if err != nil {
223 log.Errorf("getFirstSeen: %s", err.Error())
224 continue
225 }
226 out.FirstSeen = int(tmpMap["first_seen"].(int64))
227
228 outs = append(outs, out)
229 } else {
230 log.Warn("result is missing rdata column, something is very wrong")
231 }
232 } else {
233 log.Warn("result is missing rrname column, something is very wrong")
234 }
235
236 }
237
238 return outs, nil
239 }
240
241 // TotalCount returns the overall number of observations across all sensors.
242 func (db *CassandraDB) TotalCount() (int, error) {
243 // TODO
244 return 0, nil
245 }
246
247 // Shutdown closes the database connection, leaving the database unable to
248 // process both reads and writes.
249 func (db *CassandraDB) Shutdown() {
250 close(db.StopChan)
251 if db.Session != nil {
252 db.Session.Close()
253 }
254 }
0 package db
1
2 // func TestCassandraSimpleConsume(t *testing.T) {
3 // t.Skip("skipping Cassandra tests due to removed gockle support")
4 // db, m := makeCassandraDBMock()
5 // defer db.Shutdown()
6
7 // inChan := make(chan observation.InputObservation)
8 // stopChan := make(chan bool)
9
10 // go db.ConsumeFeed(inChan)
11
12 // timeStart := time.Now()
13 // timeEnd := time.Now()
14
15 // cnt := 0
16 // var iteratorMock = &gockle.IteratorMock{}
17 // iteratorMock.When("ScanMap", mock.Any).Call(func(m map[string]interface{}) bool {
18 // m["rrname"] = "foo.bar"
19 // m["rrtype"] = "A"
20 // m["rdata"] = "12.34.56.78"
21 // m["count"] = 2
22 // m["sensor_id"] = "deadcafe"
23 // cnt++
24 // return cnt < 4
25 // })
26 // iteratorMock.When("Close").Return(nil)
27
28 // cnt2 := 0
29 // var iteratorMockRdata = &gockle.IteratorMock{}
30 // iteratorMockRdata.When("ScanMap", mock.Any).Call(func(m map[string]interface{}) bool {
31 // m["rrname"] = "foo.bar"
32 // m["rrtype"] = "A"
33 // m["rdata"] = "12.34.56.79"
34 // m["count"] = 96
35 // m["sensor_id"] = "deadcafe"
36 // cnt2++
37 // return cnt2 < 2
38 // })
39 // iteratorMockRdata.When("Close").Return(nil)
40
41 // cnt3 := 0
42 // var iteratorMockCnt = &gockle.IteratorMock{}
43 // iteratorMockCnt.When("ScanMap", mock.Any).Call(func(m map[string]interface{}) bool {
44 // m["count"] = int64(3)
45 // cnt3++
46 // return cnt3 < 2
47 // })
48 // iteratorMockCnt.When("Close").Return(nil)
49
50 // cnt4 := 0
51 // var iteratorMockFirstItem = &gockle.IteratorMock{}
52 // iteratorMockFirstItem.When("ScanMap", mock.Any).Call(func(m map[string]interface{}) bool {
53 // m["rrname"] = "foo.bar"
54 // cnt4++
55 // return cnt4 < 2
56 // })
57 // iteratorMockFirstItem.When("Close").Return(nil)
58
59 // m.When("ScanIterator", "SELECT * FROM observations_by_rrname WHERE rrname = ?", mock.Any).Return(iteratorMock)
60 // m.When("Exec", "UPDATE observations_by_rdata SET first_seen = ?, last_seen = ? where rdata = ? and rrname = ? and rrtype = ? and sensor_id = ?;", mock.Any).Return()
61 // m.When("Exec", "UPDATE observations_by_rdata SET last_seen = ? where rdata = ? and rrname = ? and rrtype = ? and sensor_id = ?;", mock.Any).Return()
62 // m.When("Exec", "UPDATE observations_by_rrname SET first_seen = ?, last_seen = ? where rrname = ? and rdata = ? and rrtype = ? and sensor_id = ?;", mock.Any).Return()
63 // m.When("Exec", "UPDATE observations_by_rrname SET last_seen = ? where rrname = ? and rdata = ? and rrtype = ? and sensor_id = ?;", mock.Any).Return()
64 // m.When("ScanIterator", "SELECT * FROM observations_by_rdata WHERE rdata = ?", []interface{}{"12.34.56.79"}).Return(iteratorMockRdata)
65 // m.When("Exec", "UPDATE observations_counts SET count = count + ? where rdata = ? and rrname = ? and rrtype = ? and sensor_id = ?;", mock.Any).Return()
66 // m.When("ScanIterator", "SELECT rrname FROM observations_by_rrname WHERE rrname = ? and rdata = ? and rrtype = ? and sensor_id = ?;", mock.Any).Return(iteratorMockFirstItem)
67 // m.When("ScanIterator", "SELECT count FROM observations_counts WHERE rrname = ? AND rdata = ? AND rrtype = ? AND sensor_id = ?", mock.Any).Return(iteratorMockCnt)
68 // m.When("Close").Return()
69
70 // o := observation.InputObservation{
71 // Rrname: "foo.bar",
72 // Rrtype: "A",
73 // SensorID: "deadcafe",
74 // Rdata: "12.34.56.78",
75 // TimestampEnd: timeStart,
76 // TimestampStart: timeEnd,
77 // Count: 1,
78 // }
79 // inChan <- o
80
81 // o = observation.InputObservation{
82 // Rrname: "foo.bar",
83 // Rrtype: "MX",
84 // SensorID: "deadcafe",
85 // Rdata: "12.34.56.77",
86 // TimestampEnd: timeStart,
87 // TimestampStart: timeEnd,
88 // Count: 1,
89 // }
90 // inChan <- o
91
92 // for i := 0; i < 500; i++ {
93 // o = observation.InputObservation{
94 // Rrname: "foo.bar",
95 // Rrtype: "NS",
96 // SensorID: "deadcafe",
97 // Rdata: "12.34.56.79",
98 // TimestampEnd: timeStart,
99 // TimestampStart: timeEnd,
100 // Count: 2,
101 // }
102 // inChan <- o
103 // }
104 // close(stopChan)
105
106 // str := "foo.bar"
107 // obs, err := db.Search(nil, &str, nil, nil)
108 // if err != nil {
109 // t.Fatal(err)
110 // }
111 // if len(obs) != 3 {
112 // t.Fatalf("wrong number of results: %d", len(obs))
113 // }
114
115 // str = "12.34.56.79"
116 // obs, err = db.Search(&str, nil, nil, nil)
117 // if err != nil {
118 // t.Fatal(err)
119 // }
120 // if len(obs) != 1 {
121 // t.Fatalf("wrong number of results: %d", len(obs))
122 // }
123
124 // str = "12.34.56.79"
125 // obs, err = db.Search(&str, nil, nil, &str)
126 // if err != nil {
127 // t.Fatal(err)
128 // }
129 // if len(obs) != 0 {
130 // t.Fatalf("wrong number of results: %d", len(obs))
131 // }
132
133 // }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package db
4
5 import (
6 "fmt"
7 "runtime"
8
9 log "github.com/sirupsen/logrus"
10 yaml "gopkg.in/yaml.v2"
11 )
12
13 // Setup represents a database backend configuration, typically provided via
14 // YAML.
15 type Setup struct {
16 Database struct {
17 Name string `yaml:"name"`
18 Type string `yaml:"type"`
19 // for local storage
20 DBPath string `yaml:"db_path"`
21 // for RocksDB
22 MemtableMemBudget uint64 `yaml:"membudget"`
23 // for Cassandra
24 Hosts []string `yaml:"hosts"`
25 Username string `yaml:"username"`
26 Password string `yaml:"password"`
27 Nworkers uint `yaml:"nof_workers"`
28 } `yaml:"database"`
29 LoadedDB DB
30 }
31
32 // LoadSetup parses a given YAML description into a new Setup structure.
33 func LoadSetup(in []byte) (*Setup, error) {
34 var s Setup
35 err := yaml.Unmarshal(in, &s)
36 if err != nil {
37 return nil, err
38 }
39 if s.Database.Name == "" {
40 return nil, fmt.Errorf("database name missing")
41 }
42 if s.Database.Type == "" {
43 return nil, fmt.Errorf("database type missing")
44 }
45 switch s.Database.Type {
46 case "rocksdb":
47 if len(s.Database.DBPath) == 0 {
48 return nil, fmt.Errorf("%s: local database path missing", s.Database.Name)
49 }
50 if s.Database.MemtableMemBudget == 0 {
51 log.Infof("%s: memory budget empty, using default of 128MB", s.Database.Name)
52 s.Database.MemtableMemBudget = 128 * 1024 * 1024
53 }
54 case "cassandra":
55 if len(s.Database.Hosts) == 0 {
56 return nil, fmt.Errorf("%s: no Cassandra hosts defined", s.Database.Name)
57 }
58 if s.Database.Nworkers == 0 {
59 s.Database.Nworkers = (uint)(runtime.NumCPU() * 8)
60 log.Infof("%s: number of workers is 0 or undefined, will use default of %d", s.Database.Name, s.Database.Nworkers)
61 }
62 }
63 return &s, nil
64 }
65
66 // Run creates the necessary database objects and makes them ready to
67 // consume or provide data.
68 func (s *Setup) Run() (DB, error) {
69 log.Infof("starting database %s", s.Database.Name)
70 var db DB
71 var err error
72 switch s.Database.Type {
73 case "rocksdb":
74 db, err = MakeRocksDB(s.Database.DBPath, s.Database.MemtableMemBudget)
75 if err != nil {
76 return nil, err
77 }
78 case "cassandra":
79 db, err = MakeCassandraDB(s.Database.Hosts, s.Database.Username, s.Database.Password, s.Database.Nworkers)
80 if err != nil {
81 return nil, err
82 }
83 }
84 s.LoadedDB = db
85 return db, nil
86 }
87
88 // Stop causes a given Setup to shut down the corresponding database
89 // connections defined within.
90 func (s *Setup) Stop(stopChan chan bool) {
91 s.LoadedDB.Shutdown()
92 close(stopChan)
93 }
0 package db
1
2 import (
3 "fmt"
4 "io/ioutil"
5 "os"
6 "testing"
7 )
8
9 func TestDBConfigLoad(t *testing.T) {
10 _, err := LoadSetup([]byte("foo["))
11 if err == nil {
12 t.Fatal("did not error with invalid data")
13 }
14
15 _, err = LoadSetup([]byte(""))
16 if err == nil {
17 t.Fatal("did not error with empty config")
18 }
19
20 _, err = LoadSetup([]byte(`
21 database:
22 name: Whatever
23 `))
24 if err == nil {
25 t.Fatal("did not error with missing db type")
26 }
27
28 _, err = LoadSetup([]byte(`
29 database:
30 name: Local RocksDB
31 type: rocksdb
32 `))
33 if err == nil {
34 t.Fatal("did not error with missing path")
35 }
36
37 _, err = LoadSetup([]byte(`
38 database:
39 name: Local RocksDB
40 type: rocksdb
41 db_path: /tmp/balboa
42 `))
43 if err != nil {
44 t.Fatal("does not accept regular config: ", err)
45 }
46
47 _, err = LoadSetup([]byte(`
48 database:
49 name: Local RocksDB
50 type: rocksdb
51 db_path: /tmp/balboa
52 membudget: 1000000
53 `))
54 if err != nil {
55 t.Fatal("does not accept regular config: ", err)
56 }
57
58 _, err = LoadSetup([]byte(`
59 database:
60 name: Local Cassandra
61 type: cassandra
62 hosts: [ "127.0.0.1", "127.0.0.2", "127.0.0.3" ]
63 `))
64 if err != nil {
65 t.Fatal("does not accept regular config: ", err)
66 }
67
68 _, err = LoadSetup([]byte(`
69 database:
70 name: Local Cassandra
71 type: cassandra
72 `))
73 if err == nil {
74 t.Fatal("did not error with missing hosts")
75 }
76 }
77
78 func TestDBConfigRunRocks(t *testing.T) {
79 dbdir, err := ioutil.TempDir("", "example")
80 if err != nil {
81 t.Fatal(err)
82 }
83 dbs, err := LoadSetup([]byte(fmt.Sprintf(`
84 database:
85 name: Local RocksDB
86 type: rocksdb
87 membudget: 1000000
88 db_path: %s
89 `, dbdir)))
90 if err != nil {
91 t.Fatal("does not accept regular config: ", err)
92 }
93
94 _, err = dbs.Run()
95 if err != nil {
96 t.Fatal(err)
97 }
98 defer os.RemoveAll(dbdir)
99
100 stopChan := make(chan bool)
101 dbs.Stop(stopChan)
102 <-stopChan
103
104 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package db
4
5 import (
6 "database/sql"
7 "time"
8
9 "github.com/DCSO/balboa/observation"
10
11 // imported for side effects
12 _ "github.com/go-sql-driver/mysql"
13 log "github.com/sirupsen/logrus"
14 )
15
16 // create table observations (
17 // id bigint AUTO_INCREMENT,
18 // sensorID varchar(255) NOT NULL,
19 // rrname varchar(255) NOT NULL,
20 // rdata varchar(255) NOT NULL,
21 // rrcode varchar(64) NULL,
22 // rrtype varchar(64) NULL,
23 // count int NOT NULL,
24 // time_first timestamp NOT NULL,
25 // time_last timestamp NOT NULL,
26 // PRIMARY KEY (id),
27 // UNIQUE KEY (rdata, rrname, sensorID),
28 // INDEX rrname (rrname)
29 // );
30
31 // MySQLDB is a DB implementation based on MySQL.
32 type MySQLDB struct {
33 DB *sql.DB
34 }
35
36 const (
37 mysqlbufsize = 50
38 upsertSQL = `
39 INSERT INTO observations
40 (sensorID, rrname, rdata, rrcode, rrtype, count, time_first, time_last)
41 VALUES
42 (?, ?, ?, ?, ?, ?, ?, ?)
43 ON DUPLICATE KEY UPDATE
44 count = count + VALUES(count),
45 time_last = VALUES(time_last);`
46 qrySQL = `
47 SELECT
48 sensorID, rrname, rdata, rrtype, count, time_first, time_last
49 FROM
50 observations
51 WHERE
52 rdata like ? AND rrname like ? AND rrtype like ? AND sensorID like ?;`
53 )
54
55 // MakeMySQLDB returns a new MySQLDB instance based on the given sql.DB
56 // pointer.
57 func MakeMySQLDB(pdb *sql.DB) *MySQLDB {
58 db := &MySQLDB{
59 DB: pdb,
60 }
61 return db
62 }
63
64 // AddObservation adds a single observation synchronously to the database.
65 func (db *MySQLDB) AddObservation(obs observation.InputObservation) observation.Observation {
66 //Not implemented
67 log.Warn("AddObservation() not implemented on MySQL DB backend")
68 return observation.Observation{}
69 }
70
71 // ConsumeFeed accepts observations from a channel and queues them for
72 // database insertion.
73 func (db *MySQLDB) ConsumeFeed(inChan chan observation.InputObservation, stopChan chan bool) {
74 buf := make([]observation.InputObservation, mysqlbufsize)
75 i := 0
76 for {
77 select {
78 case <-stopChan:
79 log.Info("database ingest terminated")
80 return
81 default:
82 if i < mysqlbufsize {
83 log.Debug("buffering ", i)
84 buf[i] = <-inChan
85 i++
86 } else {
87 // flushing buffer in one transaction
88 i = 0
89 startTime := time.Now()
90 tx, err := db.DB.Begin()
91 if err != nil {
92 log.Warn(err)
93 }
94 var upsert *sql.Stmt
95 upsert, _ = tx.Prepare(upsertSQL)
96 for _, obs := range buf {
97 _, err := upsert.Exec(obs.SensorID, obs.Rrname,
98 obs.Rdata, nil, obs.Rrtype, obs.Count,
99 obs.TimestampStart, obs.TimestampEnd)
100 if err != nil {
101 log.Warn(err)
102 tx.Rollback()
103 }
104 }
105 tx.Commit()
106 log.Infof("insert Tx took %v", time.Since(startTime))
107 }
108 }
109 }
110 }
111
112 // Search returns a slice of observations matching one or more criteria such
113 // as rdata, rrname, rrtype or sensor ID.
114 func (db *MySQLDB) Search(qrdata, qrrname, qrrtype, qsensorID *string) ([]observation.Observation, error) {
115 outs := make([]observation.Observation, 0)
116 stmt, err := db.DB.Prepare(qrySQL)
117 if err != nil {
118 return nil, err
119 }
120
121 rdata := "%"
122 if qrdata != nil {
123 rdata = *qrdata
124 }
125 sensorID := "%"
126 if qsensorID != nil {
127 sensorID = *qsensorID
128 }
129 rrname := "%"
130 if qrrname != nil {
131 rrname = *qrrname
132 }
133 rrtype := "%"
134 if qrrtype != nil {
135 rrtype = *qrrtype
136 }
137
138 rows, err := stmt.Query(rdata, rrname, rrtype, sensorID)
139 if err != nil {
140 return nil, err
141 }
142
143 for rows.Next() {
144 out := observation.Observation{}
145 var lastseen, firstseen time.Time
146 err := rows.Scan(&out.SensorID, &out.RRName, &out.RData, &out.RRType,
147 &out.Count, &firstseen, &lastseen)
148 if err != nil {
149 return nil, err
150 }
151 out.FirstSeen = int(firstseen.Unix())
152 out.LastSeen = int(lastseen.Unix())
153 outs = append(outs, out)
154 }
155 err = rows.Err()
156 if err != nil {
157 return nil, err
158 }
159 return outs, nil
160 }
161
162 // TotalCount returns the overall number of observations across all sensors.
163 func (db *MySQLDB) TotalCount() (int, error) {
164 var val int
165 var err error
166 rows, err := db.DB.Query("SELECT count(*) FROM observations")
167 if err != nil {
168 return 0, err
169 }
170 defer rows.Close()
171 for rows.Next() {
172 err := rows.Scan(&val)
173 if err != nil {
174 return 0, err
175 }
176 }
177 err = rows.Err()
178 if err != nil {
179 return 0, err
180 }
181 return val, nil
182 }
183
184 // Shutdown closes the database connection, leaving the database unable to
185 // process both reads and writes.
186 func (db *MySQLDB) Shutdown() {
187 db.DB.Close()
188 }
0 package db
1
2 import (
3 "testing"
4 "time"
5
6 sqlmock "github.com/DATA-DOG/go-sqlmock"
7 "github.com/DCSO/balboa/observation"
8 )
9
10 func createMysqlTmpDB(t *testing.T) (*MySQLDB, sqlmock.Sqlmock) {
11 db, mock, err := sqlmock.New()
12 if err != nil {
13 t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
14 }
15
16 return MakeMySQLDB(db), mock
17 }
18
19 func TestMySQLSimpleConsume(t *testing.T) {
20 db, mock := createMysqlTmpDB(t)
21 defer db.Shutdown()
22
23 inChan := make(chan observation.InputObservation)
24 stopChan := make(chan bool)
25
26 go db.ConsumeFeed(inChan, stopChan)
27
28 timeStart := time.Now()
29 timeEnd := time.Now()
30 mock.ExpectBegin()
31 mock.ExpectPrepare("INSERT INTO observations")
32 mock.ExpectExec("INSERT INTO observations").WithArgs("deadcafe", "x.foo.bar", "12.34.56.78", nil, "A", 1, timeStart, timeEnd).WillReturnResult(sqlmock.NewResult(1, 1))
33 mock.ExpectExec("INSERT INTO observations").WithArgs("deadcafe", "y.foo.bar", "12.34.56.77", nil, "MX", 1, timeStart, timeEnd).WillReturnResult(sqlmock.NewResult(1, 1))
34 for i := 0; i < 48; i++ {
35 mock.ExpectExec("INSERT INTO observations").WithArgs("deadcafe", "foo.bar", "12.34.56.79", nil, "NS", 2, timeStart, timeEnd).WillReturnResult(sqlmock.NewResult(1, 1))
36 }
37 mock.ExpectPrepare("SELECT (.+) FROM observations")
38 retRows := sqlmock.NewRows(
39 []string{
40 "sensor_id",
41 "rrname",
42 "rdata",
43 "rrtype",
44 "count",
45 "time_first",
46 "time_last",
47 },
48 )
49 retRows.AddRow("12.34.56.78", "A", "x.foo.bar", "deadcafe", 1, timeStart, timeEnd)
50 retRows.AddRow("12.34.56.78", "NX", "y.foo.bar", "deadcafe", 1, timeStart, timeEnd)
51 retRows.AddRow("12.34.56.78", "NS", "foo.bar", "deadcafe", 96, timeStart, timeEnd)
52 mock.ExpectQuery("SELECT (.+) FROM observations").WillReturnRows(retRows)
53 mock.ExpectPrepare("SELECT (.+) FROM observations")
54 retRows = sqlmock.NewRows(
55 []string{
56 "sensor_id",
57 "rrname",
58 "rdata",
59 "rrtype",
60 "count",
61 "time_first",
62 "time_last",
63 },
64 )
65 retRows.AddRow("12.34.56.78", "NS", "foo.bar", "deadcafe", 96, timeStart, timeEnd)
66 mock.ExpectQuery("SELECT (.+) FROM observations").WillReturnRows(retRows)
67 mock.ExpectPrepare("SELECT (.+) FROM observations")
68 retRows = sqlmock.NewRows(
69 []string{
70 "sensor_id",
71 "rrname",
72 "rdata",
73 "rrtype",
74 "count",
75 "time_first",
76 "time_last",
77 },
78 )
79 mock.ExpectQuery("SELECT (.+) FROM observations").WillReturnRows(retRows)
80 mock.ExpectQuery("SELECT (.+) FROM observations").WillReturnRows(sqlmock.NewRows(
81 []string{
82 "count",
83 }).AddRow(3))
84
85 inChan <- observation.InputObservation{
86 Rrname: "x.foo.bar",
87 Rrtype: "A",
88 SensorID: "deadcafe",
89 Rdata: "12.34.56.78",
90 TimestampEnd: timeEnd,
91 TimestampStart: timeStart,
92 Count: 1,
93 }
94 inChan <- observation.InputObservation{
95 Rrname: "y.foo.bar",
96 Rrtype: "MX",
97 SensorID: "deadcafe",
98 Rdata: "12.34.56.77",
99 TimestampEnd: timeEnd,
100 TimestampStart: timeStart,
101 Count: 1,
102 }
103 for i := 0; i < 50; i++ {
104 inChan <- observation.InputObservation{
105 Rrname: "foo.bar",
106 Rrtype: "NS",
107 SensorID: "deadcafe",
108 Rdata: "12.34.56.79",
109 TimestampEnd: timeEnd,
110 TimestampStart: timeStart,
111 Count: 2,
112 }
113 }
114 close(stopChan)
115
116 str := "foo.bar"
117 obs, err := db.Search(nil, &str, nil, nil)
118 if err != nil {
119 t.Fatal(err)
120 }
121 if len(obs) != 3 {
122 t.Fatalf("wrong number of results: %d", len(obs))
123 }
124
125 str = "12.34.56.79"
126 obs, err = db.Search(&str, nil, nil, nil)
127 if err != nil {
128 t.Fatal(err)
129 }
130 if len(obs) != 1 {
131 t.Fatalf("wrong number of results: %d", len(obs))
132 }
133
134 str = "12.34.56.79"
135 obs, err = db.Search(&str, nil, nil, &str)
136 if err != nil {
137 t.Fatal(err)
138 }
139 if len(obs) != 0 {
140 t.Fatalf("wrong number of results: %d", len(obs))
141 }
142
143 tc, err := db.TotalCount()
144 if err != nil {
145 t.Fatal(err)
146 }
147 if tc != 3 {
148 t.Fatalf("wrong number of results: %d", len(obs))
149 }
150
151 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package db
4
5 // #cgo LDFLAGS: -lrocksdb -ltpl
6 // #cgo CFLAGS: -O3 -Wno-implicit-function-declaration -Wall -Wextra -Wno-unused-parameter
7 // #include "obs_rocksdb.h"
8 // #include <stdlib.h>
9 import "C"
10
11 import (
12 "encoding/json"
13 "fmt"
14 "strings"
15 "time"
16 "unsafe"
17
18 "github.com/DCSO/balboa/observation"
19
20 log "github.com/sirupsen/logrus"
21 )
22
23 const (
24 rocksTxSize = 50
25 keySepChar = "\x1f" // we use ASCII Unit separators
26 )
27
28 // RocksDB is a DB implementation based on Facebook's RocksDB engine.
29 type RocksDB struct {
30 db *C.ObsDB
31 stopChan chan bool
32 }
33
34 func rocksMakeKey(sensor string, rrname string, rrtype string, rdata string) string {
35 k := fmt.Sprintf("o%s%s%s%s%s%s%s%s", keySepChar, rrname, keySepChar, sensor, keySepChar, rrtype, keySepChar, rdata)
36 return k
37 }
38
39 func rocksMakeInvKey(sensor string, rrname string, rrtype string, rdata string) string {
40 k := fmt.Sprintf("i%s%s%s%s%s%s%s%s", keySepChar, rdata, keySepChar, sensor, keySepChar, rrname, keySepChar, rrtype)
41 return k
42 }
43
44 // MakeRocksDB returns a new RocksDB instance, storing data at the given dbPath.
45 // The second parameter specifies a memory budget which determines the size of
46 // memtables and caches.
47 func MakeRocksDB(dbPath string, membudget uint64) (*RocksDB, error) {
48 log.Info("opening database...")
49
50 e := C.error_new()
51 defer C.error_delete(e)
52 cdbPath := C.CString(dbPath)
53 cdb := C.obsdb_open(cdbPath, C.size_t(membudget), e)
54 defer C.free(unsafe.Pointer(cdbPath))
55
56 if C.error_is_set(e) {
57 return nil, fmt.Errorf("%s", C.GoString(C.error_get(e)))
58 }
59
60 db := &RocksDB{
61 db: cdb,
62 stopChan: make(chan bool),
63 }
64
65 log.Info("database ready")
66 return db, nil
67 }
68
69 // MakeRocksDBReadonly returns a new read-only RocksDB instance.
70 func MakeRocksDBReadonly(dbPath string) (*RocksDB, error) {
71 log.Info("opening database...")
72
73 e := C.error_new()
74 defer C.error_delete(e)
75 cdbPath := C.CString(dbPath)
76 cdb := C.obsdb_open_readonly(cdbPath, e)
77 defer C.free(unsafe.Pointer(cdbPath))
78
79 if C.error_is_set(e) {
80 return nil, fmt.Errorf("%s", C.GoString(C.error_get(e)))
81 }
82
83 db := &RocksDB{
84 db: cdb,
85 stopChan: make(chan bool),
86 }
87
88 log.Info("database ready")
89 return db, nil
90 }
91
92 func rocksTxDedup(in []observation.InputObservation) []observation.InputObservation {
93 cache := make(map[string]*observation.InputObservation)
94 for i, inObs := range in {
95 key := rocksMakeKey(inObs.SensorID, inObs.Rrname, inObs.Rrtype, inObs.Rdata)
96 _, ok := cache[key]
97 if ok {
98 cache[key].Count += inObs.Count
99 cache[key].TimestampEnd = inObs.TimestampEnd
100 } else {
101 cache[key] = &in[i]
102 }
103 }
104 out := make([]observation.InputObservation, 0)
105 for _, v := range cache {
106 out = append(out, *v)
107 }
108 if len(in) != len(out) {
109 log.Debugf("TX dedup: %d -> %d", len(in), len(out))
110 }
111 return out
112 }
113
114 // AddObservation adds a single observation synchronously to the database.
115 func (db *RocksDB) AddObservation(obs observation.InputObservation) observation.Observation {
116 var cobs C.Observation
117 e := C.error_new()
118 defer C.error_delete(e)
119
120 key := rocksMakeKey(obs.SensorID, obs.Rrname, obs.Rrtype,
121 obs.Rdata)
122 invKey := rocksMakeInvKey(obs.SensorID, obs.Rrname,
123 obs.Rrtype, obs.Rdata)
124
125 cobs.key = C.CString(key)
126 cobs.inv_key = C.CString(invKey)
127 cobs.count = C.uint(obs.Count)
128 cobs.last_seen = C.uint(obs.TimestampEnd.Unix())
129 cobs.first_seen = C.uint(obs.TimestampStart.Unix())
130
131 C.obsdb_put(db.db, &cobs, e)
132 if C.error_is_set(e) {
133 log.Errorf("%s", C.GoString(C.error_get(e)))
134 }
135
136 C.free(unsafe.Pointer(cobs.key))
137 C.free(unsafe.Pointer(cobs.inv_key))
138
139 r, err := db.Search(&obs.Rdata, &obs.Rrname, &obs.Rrtype, &obs.SensorID)
140 if err != nil {
141 log.Error(err)
142 }
143 return r[0]
144 }
145
146 type rocksObservation struct {
147 Count int
148 LastSeen int64
149 FirstSeen int64
150 }
151
152 // ConsumeFeed accepts observations from a channel and queues them for
153 // database insertion.
154 func (db *RocksDB) ConsumeFeed(inChan chan observation.InputObservation) {
155 var i = 0
156 buf := make([]observation.InputObservation, rocksTxSize)
157 var cobs C.Observation
158 e := C.error_new()
159 defer C.error_delete(e)
160
161 for {
162 select {
163 case <-db.stopChan:
164 log.Info("database ingest terminated")
165 return
166 default:
167 obs := <-inChan
168 if i < rocksTxSize {
169 buf[i] = obs
170 i++
171 } else {
172 i = 0
173 startTime := time.Now()
174 for _, obs := range rocksTxDedup(buf) {
175 if obs.Rrtype == "" {
176 continue
177 }
178
179 key := rocksMakeKey(obs.SensorID, obs.Rrname, obs.Rrtype,
180 obs.Rdata)
181 invKey := rocksMakeInvKey(obs.SensorID, obs.Rrname,
182 obs.Rrtype, obs.Rdata)
183
184 cobs.key = C.CString(key)
185 cobs.inv_key = C.CString(invKey)
186 cobs.count = C.uint(obs.Count)
187 cobs.last_seen = C.uint(obs.TimestampEnd.Unix())
188 cobs.first_seen = C.uint(obs.TimestampStart.Unix())
189
190 C.obsdb_put(db.db, &cobs, e)
191 if C.error_is_set(e) {
192 log.Errorf("%s", C.GoString(C.error_get(e)))
193 }
194
195 C.free(unsafe.Pointer(cobs.key))
196 C.free(unsafe.Pointer(cobs.inv_key))
197 }
198 log.Debugf("insert Tx took %v", time.Since(startTime))
199 }
200 }
201 }
202 }
203
204 //export cgoLogInfo
205 func cgoLogInfo(str *C.char) {
206 log.Info(C.GoString(str))
207 }
208
209 //export cgoLogDebug
210 func cgoLogDebug(str *C.char) {
211 log.Debug(C.GoString(str))
212 }
213
214 // Search returns a slice of observations matching one or more criteria such
215 // as rdata, rrname, rrtype or sensor ID.
216 func (db *RocksDB) Search(qrdata, qrrname, qrrtype, qsensorID *string) ([]observation.Observation, error) {
217 outs := make([]observation.Observation, 0)
218 var cqrdata, cqrrname, cqrrtype, cqsensorID *C.char
219 var i uint
220
221 if qrdata == nil {
222 cqrdata = nil
223 } else {
224 cqrdata = C.CString(*qrdata)
225 defer C.free(unsafe.Pointer(cqrdata))
226 }
227 if qrrname == nil {
228 cqrrname = nil
229 } else {
230 cqrrname = C.CString(*qrrname)
231 defer C.free(unsafe.Pointer(cqrrname))
232 }
233 if qrrtype == nil {
234 cqrrtype = nil
235 } else {
236 cqrrtype = C.CString(*qrrtype)
237 defer C.free(unsafe.Pointer(cqrrtype))
238 }
239 if qsensorID == nil {
240 cqsensorID = nil
241 } else {
242 cqsensorID = C.CString(*qsensorID)
243 defer C.free(unsafe.Pointer(cqsensorID))
244 }
245
246 r := C.obsdb_search(db.db, cqrdata, cqrrname, cqrrtype, cqsensorID)
247 defer C.obs_set_delete(r)
248
249 for i = 0; i < uint(C.obs_set_size(r)); i++ {
250 o := C.obs_set_get(r, C.ulong(i))
251 valArr := strings.Split(C.GoString(o.key), keySepChar)
252 outs = append(outs, observation.Observation{
253 SensorID: valArr[2],
254 Count: int(o.count),
255 RData: valArr[4],
256 RRName: valArr[1],
257 RRType: valArr[3],
258 FirstSeen: int(o.first_seen),
259 LastSeen: int(o.last_seen),
260 })
261 }
262
263 return outs, nil
264 }
265
266 //export cgoObsDump
267 func cgoObsDump(o *C.Observation) {
268 valArr := strings.Split(C.GoString(o.key), keySepChar)
269 myObs := observation.Observation{
270 SensorID: valArr[2],
271 Count: int(o.count),
272 RData: valArr[4],
273 RRName: valArr[1],
274 RRType: valArr[3],
275 FirstSeen: int(o.first_seen),
276 LastSeen: int(o.last_seen),
277 }
278 js, err := json.Marshal(myObs)
279 if err == nil {
280 fmt.Println(string(js))
281 }
282 }
283
284 // Dump prints all aggregated observations in the database to stdout, in JSON format.
285 func (db *RocksDB) Dump() error {
286 e := C.error_new()
287 defer C.error_delete(e)
288
289 C.obsdb_dump(db.db, e)
290
291 if C.error_is_set(e) {
292 return fmt.Errorf("%s", C.GoString(C.error_get(e)))
293 }
294
295 return nil
296 }
297
298 // TotalCount returns the overall number of observations across all sensors.
299 func (db *RocksDB) TotalCount() (int, error) {
300 var val int
301 val = int(C.obsdb_num_keys(db.db))
302 return val, nil
303 }
304
305 // Shutdown closes the database connection, leaving the database unable to
306 // process both reads and writes.
307 func (db *RocksDB) Shutdown() {
308 close(db.stopChan)
309 C.obsdb_close(db.db)
310 log.Info("database closed")
311 }
0 package db
1
2 import (
3 "io/ioutil"
4 "os"
5 "syscall"
6 "testing"
7 "time"
8
9 "github.com/DCSO/balboa/observation"
10 )
11
12 func createRocksTmpDB(t *testing.T) (*RocksDB, string) {
13 dbdir, err := ioutil.TempDir("", "example")
14 if err != nil {
15 t.Fatal(err)
16 }
17 db, err := MakeRocksDB(dbdir, 8000000)
18 if err != nil {
19 t.Fatal(err)
20 }
21 if db == nil {
22 t.Fatal("no db created")
23 }
24 return db, dbdir
25 }
26
27 func TestRocksCreate(t *testing.T) {
28 db, dbdir := createRocksTmpDB(t)
29 defer os.RemoveAll(dbdir)
30 defer db.Shutdown()
31 }
32
33 func TestRocksCreateFail(t *testing.T) {
34 // skip this test if run as root
35 if syscall.Getuid() == 0 {
36 t.Skip()
37 }
38 _, err := MakeRocksDB("/nonexistent", 8000000)
39 if err == nil {
40 t.Fatal(err)
41 }
42 }
43
44 func TestRocksSimpleStore(t *testing.T) {
45 db, dbdir := createRocksTmpDB(t)
46 defer os.RemoveAll(dbdir)
47 defer db.Shutdown()
48
49 a := db.AddObservation(observation.InputObservation{
50 Rrname: "foo.bar",
51 Rrtype: "A",
52 SensorID: "deadcafe",
53 Rdata: "12.34.56.78",
54 TimestampEnd: time.Now(),
55 TimestampStart: time.Now(),
56 Count: 1,
57 })
58 if a.RData != "12.34.56.78" || a.RRName != "foo.bar" || a.RRType != "A" ||
59 a.Count != 1 {
60 t.Fatal("invalid return")
61 }
62 a = db.AddObservation(observation.InputObservation{
63 Rrname: "foo.bar",
64 Rrtype: "MX",
65 SensorID: "deadcafe",
66 Rdata: "12.34.56.77",
67 TimestampEnd: time.Now(),
68 TimestampStart: time.Now(),
69 Count: 1,
70 })
71 if a.RData != "12.34.56.77" || a.RRName != "foo.bar" || a.RRType != "MX" ||
72 a.Count != 1 {
73 t.Fatal("invalid return")
74 }
75 a = db.AddObservation(observation.InputObservation{
76 Rrname: "foo.bar",
77 Rrtype: "NS",
78 SensorID: "deadcafe",
79 Rdata: "12.34.56.79",
80 TimestampEnd: time.Now(),
81 TimestampStart: time.Now(),
82 Count: 1,
83 })
84 if a.RData != "12.34.56.79" || a.RRName != "foo.bar" || a.RRType != "NS" ||
85 a.Count != 1 {
86 t.Fatal("invalid return")
87 }
88
89 str := "foo.bar"
90 obs, err := db.Search(nil, &str, nil, nil)
91 if err != nil {
92 t.Fatal(err)
93 }
94 if len(obs) != 3 {
95 t.Fatalf("wrong number of results: %d", len(obs))
96 }
97
98 a = db.AddObservation(observation.InputObservation{
99 Rrname: "foo.bar",
100 Rrtype: "NS",
101 SensorID: "deadcafe",
102 Rdata: "12.34.56.79",
103 TimestampEnd: time.Now().Add(77 * time.Second),
104 TimestampStart: time.Now().Add(77 * time.Second),
105 Count: 4,
106 })
107 if a.RData != "12.34.56.79" || a.RRName != "foo.bar" || a.RRType != "NS" ||
108 a.Count != 5 {
109 t.Fatalf("invalid return %v", a)
110 }
111
112 obs, err = db.Search(nil, &str, nil, nil)
113 if err != nil {
114 t.Fatal(err)
115 }
116 if len(obs) != 3 {
117 t.Fatalf("wrong number of results: %d", len(obs))
118 }
119 }
120
121 func TestRocksSimpleConsume(t *testing.T) {
122 db, dbdir := createRocksTmpDB(t)
123 defer os.RemoveAll(dbdir)
124 defer db.Shutdown()
125
126 inChan := make(chan observation.InputObservation)
127 stopChan := make(chan bool)
128
129 go db.ConsumeFeed(inChan)
130
131 inChan <- observation.InputObservation{
132 Rrname: "foo.bar",
133 Rrtype: "A",
134 SensorID: "deadcafe",
135 Rdata: "12.34.56.78",
136 TimestampEnd: time.Now(),
137 TimestampStart: time.Now(),
138 Count: 1,
139 }
140 inChan <- observation.InputObservation{
141 Rrname: "foo.bar",
142 Rrtype: "MX",
143 SensorID: "deadcafe",
144 Rdata: "12.34.56.77",
145 TimestampEnd: time.Now(),
146 TimestampStart: time.Now(),
147 Count: 1,
148 }
149 for i := 0; i < 10000; i++ {
150 inChan <- observation.InputObservation{
151 Rrname: "foo.bar",
152 Rrtype: "NS",
153 SensorID: "deadcafe",
154 Rdata: "12.34.56.79",
155 TimestampEnd: time.Now(),
156 TimestampStart: time.Now(),
157 Count: 2,
158 }
159 }
160 close(stopChan)
161
162 str := "foo.bar"
163 obs, err := db.Search(nil, &str, nil, nil)
164 if err != nil {
165 t.Fatal(err)
166 }
167 if len(obs) != 3 {
168 t.Fatalf("wrong number of results: %d", len(obs))
169 }
170
171 str = "12.34.56.79"
172 obs, err = db.Search(&str, nil, nil, nil)
173 if err != nil {
174 t.Fatal(err)
175 }
176 if len(obs) != 1 {
177 t.Fatalf("wrong number of results: %d", len(obs))
178 }
179
180 str = "12.34.56.79"
181 obs, err = db.Search(&str, nil, nil, &str)
182 if err != nil {
183 t.Fatal(err)
184 }
185 if len(obs) != 0 {
186 t.Fatalf("wrong number of results: %d", len(obs))
187 }
188
189 }
0 /*
1 balboa
2 Copyright (c) 2018, DCSO GmbH
3 */
4
5 #include "obs_rocksdb.h"
6
7 #include <stdlib.h>
8 #include <stdio.h>
9 #include <string.h>
10 #include "rocksdb/c.h"
11 #include "tpl.h"
12
13 extern void cgoLogInfo(const char*);
14 extern void cgoLogDebug(const char*);
15 extern void cgoObsDump(Observation*);
16
17 #define OBS_SET_STEP_SIZE 1000
18
19 struct Error {
20 char *msg;
21 };
22
23 Error* error_new()
24 {
25 Error *e = calloc(1, sizeof(Error));
26 if (!e) {
27 return NULL;
28 }
29 return e;
30 }
31
32 const char* error_get(Error *e)
33 {
34 if (!e)
35 return NULL;
36 return e->msg;
37 }
38
39 static void error_set(Error *e, const char *msg)
40 {
41 if (!e)
42 return;
43 if (!msg)
44 return;
45 if (e->msg)
46 free(e->msg);
47 e->msg = strdup(msg);
48 }
49
50 void error_unset(Error *e)
51 {
52 if (!e)
53 return;
54 if (e->msg)
55 free(e->msg);
56 e->msg = NULL;
57 }
58
59 bool error_is_set(Error *e)
60 {
61 if (!e)
62 return NULL;
63 return (e->msg != NULL);
64 }
65
66 void error_delete(Error *e)
67 {
68 if (!e)
69 return;
70 if (e->msg)
71 free(e->msg);
72 free(e);
73 }
74
75
76 struct ObsSet {
77 unsigned long size, used;
78 Observation **os;
79 };
80
81 static ObsSet* obs_set_create(unsigned long size)
82 {
83 ObsSet *os = calloc((size_t) 1, sizeof(ObsSet));
84 if (!os)
85 return NULL;
86 os->size = size;
87 os->used = 0;
88 os->os = calloc((size_t) size, sizeof(Observation*));
89 if (os->os == NULL) {
90 free(os);
91 return NULL;
92 }
93 return os;
94 }
95
96 unsigned long obs_set_size(ObsSet *os)
97 {
98 if (!os)
99 return 0;
100 return os->used;
101 }
102
103 static void obs_set_add(ObsSet *os, Observation *o)
104 {
105 if (!os || !os->os)
106 return;
107 if (os->used == os->size) {
108 os->size += OBS_SET_STEP_SIZE;
109 os->os = realloc(os->os, (size_t) (os->size * sizeof(Observation*)));
110 }
111 os->os[os->used++] = o;
112 }
113
114 const Observation* obs_set_get(ObsSet *os, unsigned long i)
115 {
116 if (!os)
117 return NULL;
118 if (i >= os->used)
119 return NULL;
120 return os->os[i];
121 }
122
123 void obs_set_delete(ObsSet *os)
124 {
125 if (!os)
126 return;
127 if (os->os != NULL) {
128 unsigned long i = 0;
129 for (i = 0; i < os->used; i++) {
130 if (os->os[i]->key)
131 free(os->os[i]->key);
132 if (os->os[i]->inv_key)
133 free(os->os[i]->inv_key);
134 free(os->os[i]);
135 }
136 free(os->os);
137 }
138 free(os);
139 }
140
141 struct ObsDB {
142 rocksdb_t *db;
143 rocksdb_options_t *options;
144 rocksdb_writeoptions_t *writeoptions;
145 rocksdb_readoptions_t *readoptions;
146 rocksdb_mergeoperator_t *mergeop;
147 };
148
149 #define VALUE_LENGTH (sizeof(uint32_t) + (2 * sizeof(time_t)))
150
151 #define obsdb_max(a,b) \
152 ({ __typeof__ (a) _a = (a); \
153 __typeof__ (b) _b = (b); \
154 _a > _b ? _a : _b; })
155
156 #define obsdb_min(a,b) \
157 ({ __typeof__ (a) _a = (a); \
158 __typeof__ (b) _b = (b); \
159 _a < _b ? _a : _b; })
160
161 static inline int obs2buf(Observation *o, char **buf, size_t *buflen) {
162 uint32_t a,b,c;
163 int ret = 0;
164 tpl_node *tn = tpl_map("uuu", &a, &b, &c);
165 a = o->count;
166 b = o->last_seen;
167 c = o->first_seen;
168 ret = tpl_pack(tn, 0);
169 if (ret == 0) {
170 ret = tpl_dump(tn, TPL_MEM, buf, buflen);
171 }
172 tpl_free(tn);
173 return ret;
174 }
175
176 static inline int buf2obs(Observation *o, const char *buf, size_t buflen) {
177 uint32_t a,b,c;
178 int ret = 0;
179 tpl_node *tn = tpl_map("uuu", &a, &b, &c);
180 ret = tpl_load(tn, TPL_MEM, buf, buflen);
181 if (ret == 0) {
182 (void) tpl_unpack(tn, 0);
183 o->count = a;
184 o->last_seen = b;
185 o->first_seen = c;
186 }
187 tpl_free(tn);
188 return ret;
189 }
190
191 static char* obsdb_mergeop_full_merge(void *state, const char* key,
192 size_t key_length, const char* existing_value,
193 size_t existing_value_length,
194 const char* const* operands_list,
195 const size_t* operands_list_length,
196 int num_operands, unsigned char* success,
197 size_t* new_value_length)
198 {
199 Observation obs = {NULL, NULL, 0, 0, 0};
200
201 if (key_length < 1) {
202 fprintf(stderr, "full merge: key too short\n");
203 *success = (unsigned char) 0;
204 return NULL;
205 }
206 if (key[0] == 'i') {
207 /* this is an inverted index key with no meaningful value */
208 char *res = malloc(1 * sizeof(char));
209 *res = '\0';
210 *new_value_length = 1;
211 *success = 1;
212 return res;
213 } else if (key[0] == 'o') {
214 /* this is an observation value */
215 int i;
216 size_t buflength;
217 char *buf = NULL;
218 if (existing_value) {
219 buf2obs(&obs, existing_value, existing_value_length);
220 }
221 for (i = 0; i < num_operands; i++) {
222 Observation nobs = {NULL, NULL, 0, 0, 0};
223 buf2obs(&nobs, operands_list[i], operands_list_length[i]);
224 if (i == 0) {
225 if (!existing_value) {
226 obs.count = nobs.count;
227 obs.last_seen = nobs.last_seen;
228 obs.first_seen = nobs.first_seen;
229 } else {
230 obs.count += nobs.count;
231 obs.last_seen = obsdb_max(obs.last_seen, nobs.last_seen);
232 obs.first_seen = obsdb_min(obs.first_seen, nobs.first_seen);
233 }
234 } else {
235 obs.count += nobs.count;
236 obs.last_seen = obsdb_max(obs.last_seen, nobs.last_seen);
237 obs.first_seen = obsdb_min(obs.first_seen, nobs.first_seen);
238 }
239 }
240 obs2buf(&obs, &buf, &buflength);
241 *new_value_length = buflength;
242 *success = (unsigned char) 1;
243 return buf;
244 } else {
245 /* weird key format! */
246 fprintf(stderr, "full merge: weird key format\n");
247 *success = (unsigned char) 0;
248 return NULL;
249 }
250 }
251
252 static char* obsdb_mergeop_partial_merge(void *state, const char* key,
253 size_t key_length,
254 const char* const* operands_list,
255 const size_t* operands_list_length,
256 int num_operands, unsigned char* success,
257 size_t* new_value_length)
258 {
259 Observation obs = {NULL, NULL, 0, 0, 0};
260
261 if (key_length < 1) {
262 fprintf(stderr, "partial merge: key too short\n");
263 *success = (unsigned char) 0;
264 return NULL;
265 }
266 if (key[0] == 'i') {
267 /* this is an inverted index key with no meaningful value */
268 char *res = malloc(1 * sizeof(char));
269 *res = '\0';
270 *new_value_length = 1;
271 *success = 1;
272 return res;
273 } else if (key[0] == 'o') {
274 /* this is an observation value */
275 int i;
276 size_t buflength;
277 char *buf = NULL;
278 for (i = 0; i < num_operands; i++) {
279 Observation nobs = {NULL, NULL, 0, 0, 0};
280 buf2obs(&nobs, operands_list[i], operands_list_length[i]);
281 if (i == 0) {
282 obs.count = nobs.count;
283 obs.last_seen = nobs.last_seen;
284 obs.first_seen = nobs.first_seen;
285 } else {
286 obs.count += nobs.count;
287 obs.last_seen = obsdb_max(obs.last_seen, nobs.last_seen);
288 obs.first_seen = obsdb_min(obs.first_seen, nobs.first_seen);
289 }
290 }
291 obs2buf(&obs, &buf, &buflength);
292 *new_value_length = buflength;
293 *success = (unsigned char) 1;
294 return buf;
295 } else {
296 /* weird key format! */
297 fprintf(stderr, "partial merge: weird key format\n");
298 *success = (unsigned char) 0;
299 return NULL;
300 }
301 }
302
303 static void obsdb_mergeop_destructor(void *state)
304 {
305 return;
306 }
307
308 static const char* obsdb_mergeop_name(void *state)
309 {
310 return "observation mergeop";
311 }
312
313 static ObsDB* _obsdb_open(const char *path, size_t membudget, Error *e, bool readonly)
314 {
315 char *err = NULL;
316 int level_compression[5] = {
317 rocksdb_lz4_compression,
318 rocksdb_lz4_compression,
319 rocksdb_lz4_compression,
320 rocksdb_lz4_compression,
321 rocksdb_lz4_compression
322 };
323 ObsDB *db = calloc(1, sizeof(ObsDB));
324 if (db == NULL) {
325 if (e)
326 error_set(e, "could not allocate memory");
327 return NULL;
328 }
329
330 db->mergeop = rocksdb_mergeoperator_create(NULL,
331 obsdb_mergeop_destructor,
332 obsdb_mergeop_full_merge,
333 obsdb_mergeop_partial_merge,
334 NULL,
335 obsdb_mergeop_name);
336
337 db->options = rocksdb_options_create();
338 rocksdb_options_increase_parallelism(db->options, 8);
339 if (!readonly)
340 rocksdb_options_optimize_level_style_compaction(db->options, membudget);
341 rocksdb_options_set_create_if_missing(db->options, 1);
342 rocksdb_options_set_max_log_file_size(db->options, 10*1024*1024);
343 rocksdb_options_set_keep_log_file_num(db->options, 2);
344 rocksdb_options_set_max_open_files(db->options, 300);
345 rocksdb_options_set_merge_operator(db->options, db->mergeop);
346 rocksdb_options_set_compression_per_level(db->options, level_compression, 5);
347
348 if (!readonly)
349 db->db = rocksdb_open(db->options, path, &err);
350 else
351 db->db = rocksdb_open_for_read_only(db->options, path, 0, &err);
352 if (err) {
353 if (e)
354 error_set(e, err);
355 free(err);
356 return NULL;
357 }
358
359 db->writeoptions = rocksdb_writeoptions_create();
360 db->readoptions = rocksdb_readoptions_create();
361
362 return db;
363 }
364
365 ObsDB* obsdb_open(const char *path, size_t membudget, Error *e) {
366 return _obsdb_open(path, membudget, e, false);
367 }
368
369 ObsDB* obsdb_open_readonly(const char *path, Error *e) {
370 return _obsdb_open(path, 0, e, true);
371 }
372
373 int obsdb_put(ObsDB *db, Observation *obs, Error *e)
374 {
375 char *err = NULL;
376 size_t buflength;
377 char *buf;
378 if (!db)
379 return -1;
380
381 (void) obs2buf(obs, &buf, &buflength);
382
383 rocksdb_merge(db->db, db->writeoptions, obs->key, strlen(obs->key),
384 buf, buflength, &err);
385 if (err) {
386 if (e)
387 error_set(e, err);
388 free(err);
389 free(buf);
390 return -1;
391
392 }
393 free(buf);
394
395 rocksdb_merge(db->db, db->writeoptions, obs->inv_key, strlen(obs->key),
396 "", 0, &err);
397 if (err) {
398 if (e)
399 error_set(e, err);
400 free(err);
401 return -1;
402 }
403
404 return 0;
405 }
406
407 int obsdb_dump(ObsDB *db, Error *e)
408 {
409 rocksdb_iterator_t *it;
410 if (!db)
411 return -1;
412
413 it = rocksdb_create_iterator(db->db, db->readoptions);
414 for (rocksdb_iter_seek(it, "o", 1);
415 rocksdb_iter_valid(it) != (unsigned char) 0;
416 rocksdb_iter_next(it)) {
417 size_t size = 0;
418 int ret = 0;
419 Observation *o = NULL;
420 const char *rkey = NULL, *val = NULL;
421
422 rkey = rocksdb_iter_key(it, &size);
423 o = calloc(1, sizeof(Observation));
424 if (!o) {
425 return -1;
426 }
427 o->key = calloc(size + 1, sizeof(char));
428 if (!o->key) {
429 free(o);
430 return -1;
431 }
432 strncpy(o->key, rkey, size);
433 o->key[size] = '\0';
434 val = rocksdb_iter_value(it, &size);
435 ret = buf2obs(o, val, size);
436 if (ret != 0) {
437 fprintf(stderr, "error\n");
438 }
439 cgoObsDump(o);
440 free(o->key);
441 free(o);
442 }
443 rocksdb_iter_destroy(it);
444
445 return 0;
446 }
447
448 ObsSet* obsdb_search(ObsDB *db, const char *qrdata, const char *qrrname,
449 const char *qrrtype, const char *qsensorID)
450 {
451 rocksdb_iterator_t *it;
452 ObsSet *os;
453 if (!db)
454 return NULL;
455
456 os = obs_set_create(OBS_SET_STEP_SIZE);
457
458 if (qrrname != NULL) {
459 char *prefix = NULL;
460 size_t prefixlen = 0;
461 if (qsensorID != NULL) {
462 prefixlen = strlen(qsensorID) + strlen(qrrname) + 4;
463 prefix = calloc(prefixlen, sizeof(char));
464 if (!prefix) {
465 obs_set_delete(os);
466 return NULL;
467 }
468 (void) snprintf(prefix, prefixlen, "o%c%s%c%s%c", 0x1f, qrrname, 0x1f, qsensorID, 0x1f);
469 } else {
470 prefixlen = strlen(qrrname) + 3;
471 prefix = calloc(prefixlen, sizeof(char));
472 if (!prefix) {
473 obs_set_delete(os);
474 return NULL;
475 }
476 (void) snprintf(prefix, prefixlen, "o%c%s%c", 0x1f, qrrname, 0x1f);
477 }
478 cgoLogDebug(prefix);
479
480 it = rocksdb_create_iterator(db->db, db->readoptions);
481 rocksdb_iter_seek(it, prefix, prefixlen);
482 for (; rocksdb_iter_valid(it) != (unsigned char) 0; rocksdb_iter_next(it)) {
483 size_t size = 0;
484 int ret = 0;
485 Observation *o = NULL;
486 const char *rkey = rocksdb_iter_key(it, &size), *val = NULL;
487 char *rrname = NULL, *sensorID = NULL, *rrtype = NULL, *rdata = NULL, *saveptr;
488 char *tokkey = calloc(size + 1, sizeof(char));
489 if (!tokkey) {
490 obs_set_delete(os);
491 rocksdb_iter_destroy(it);
492 free(prefix);
493 return NULL;
494 }
495
496 strncpy(tokkey, rkey, size);
497 tokkey[size] = '\0';
498 rrname = strtok_r(tokkey+2, "\x1f", &saveptr);
499 sensorID = strtok_r(NULL, "\x1f", &saveptr);
500 rrtype = strtok_r(NULL, "\x1f", &saveptr);
501 rdata = strtok_r(NULL, "\x1f", &saveptr);
502 if (rrname == NULL || (strcmp(rrname, qrrname) != 0)) {
503 free(tokkey);
504 tokkey = NULL;
505 break;
506 }
507 if (sensorID == NULL || (qsensorID != NULL && strcmp(qsensorID, sensorID) != 0)) {
508 free(tokkey);
509 tokkey = NULL;
510 continue;
511 }
512 if (rdata == NULL || (qrdata != NULL && strcmp(qrdata, rdata) != 0)) {
513 free(tokkey);
514 tokkey = NULL;
515 continue;
516 }
517 if (rrtype == NULL || (qrrtype != NULL && strcmp(qrrtype, rrtype) != 0)) {
518 free(tokkey);
519 tokkey = NULL;
520 continue;
521 }
522
523 o = calloc(1, sizeof(Observation));
524 if (!o) {
525 free(tokkey);
526 obs_set_delete(os);
527 rocksdb_iter_destroy(it);
528 free(prefix);
529 return NULL;
530 }
531 o->key = calloc(size + 1, sizeof(char));
532 if (!o->key) {
533 free(o);
534 free(tokkey);
535 obs_set_delete(os);
536 rocksdb_iter_destroy(it);
537 free(prefix);
538 return NULL;
539 }
540 strncpy(o->key, rkey, size);
541 o->key[size] = '\0';
542 val = rocksdb_iter_value(it, &size);
543 ret = buf2obs(o, val, size);
544 if (ret == 0) {
545 obs_set_add(os, o);
546 }
547 free(tokkey);
548 tokkey = NULL;
549 }
550 rocksdb_iter_destroy(it);
551 free(prefix);
552 } else {
553 char *prefix = NULL;
554 size_t prefixlen = 0;
555 if (qsensorID != NULL) {
556 prefixlen = strlen(qsensorID) + strlen(qrdata) + 4;
557 prefix = calloc(prefixlen, sizeof(char));
558 if (!prefix) {
559 obs_set_delete(os);
560 return NULL;
561 }
562 (void) snprintf(prefix, prefixlen, "i%c%s%c%s%c", 0x1f, qrdata, 0x1f, qsensorID, 0x1f);
563 } else {
564 prefixlen = strlen(qrdata) + 3;
565 prefix = calloc(prefixlen, sizeof(char));
566 if (!prefix) {
567 obs_set_delete(os);
568 return NULL;
569 }
570 (void) snprintf(prefix, prefixlen, "i%c%s%c", 0x1f, qrdata, 0x1f);
571 }
572 cgoLogDebug(prefix);
573
574 it = rocksdb_create_iterator(db->db, db->readoptions);
575 rocksdb_iter_seek(it, prefix, strlen(prefix));
576 for (; rocksdb_iter_valid(it) != (unsigned char) 0; rocksdb_iter_next(it)) {
577 size_t size = 0, fullkeylen;
578 int ret;
579 const char *rkey = rocksdb_iter_key(it, &size);
580 char *val = NULL;
581 char *rrname = NULL, *sensorID = NULL, *rrtype = NULL, *rdata = NULL, *saveptr;
582 char fullkey[4096];
583 char *err = NULL;
584 Observation *o = NULL;
585 char *tokkey = calloc(size + 1, sizeof(char));
586 if (!tokkey) {
587 obs_set_delete(os);
588 rocksdb_iter_destroy(it);
589 free(prefix);
590 return NULL;
591 }
592
593 strncpy(tokkey, rkey, size);
594 tokkey[size] = '\0';
595 rdata = strtok_r(tokkey+2, "\x1f", &saveptr);
596 sensorID = strtok_r(NULL, "\x1f", &saveptr);
597 rrname = strtok_r(NULL, "\x1f", &saveptr);
598 rrtype = strtok_r(NULL, "\x1f", &saveptr);
599 if (strcmp(rdata, qrdata) != 0) {
600 free(tokkey);
601 tokkey = NULL;
602 break;
603 }
604 cgoLogDebug(rdata);
605 if (sensorID == NULL || (qsensorID != NULL && strcmp(qsensorID, sensorID) != 0)) {
606 free(tokkey);
607 tokkey = NULL;
608 continue;
609 }
610 if (rdata == NULL || (qrdata != NULL && strcmp(qrdata, rdata) != 0)) {
611 free(tokkey);
612 tokkey = NULL;
613 continue;
614 }
615 if (rrtype == NULL || (qrrtype != NULL && strcmp(qrrtype, rrtype) != 0)) {
616 free(tokkey);
617 tokkey = NULL;
618 continue;
619 }
620
621 (void) snprintf(fullkey, 4096, "o%c%s%c%s%c%s%c%s", 0x1f, rrname, 0x1f, sensorID, 0x1f, rrtype, 0x1f, rdata);
622 cgoLogDebug(fullkey);
623
624 fullkeylen = strlen(fullkey);
625 val = rocksdb_get(db->db, db->readoptions, fullkey, fullkeylen, &size, &err);
626 if (err != NULL) {
627 cgoLogDebug("observation not found");
628 free(tokkey);
629 tokkey = NULL;
630 continue;
631 }
632
633 o = calloc(1, sizeof(Observation));
634 if (!o) {
635 free(tokkey);
636 obs_set_delete(os);
637 rocksdb_iter_destroy(it);
638 free(prefix);
639 free(val);
640 return NULL;
641 }
642 o->key = calloc(fullkeylen + 1, sizeof(char));
643 if (!o->key) {
644 free(o);
645 free(tokkey);
646 obs_set_delete(os);
647 rocksdb_iter_destroy(it);
648 free(prefix);
649 free(val);
650 return NULL;
651 }
652 strncpy(o->key, fullkey, fullkeylen);
653 o->key[fullkeylen] = '\0';
654 ret = buf2obs(o, val, size);
655 if (ret == 0) {
656 obs_set_add(os, o);
657 }
658 free(tokkey);
659 free(val);
660 tokkey = NULL;
661 }
662 rocksdb_iter_destroy(it);
663 free(prefix);
664 }
665
666 return os;
667 }
668
669 unsigned long obsdb_num_keys(ObsDB *db)
670 {
671 const char *val;
672 if (!db)
673 return 0;
674 val = rocksdb_property_value(db->db, "rocksdb.estimate-num-keys");
675 if (!val) {
676 return 0;
677 } else {
678 unsigned long v;
679 int ret;
680 ret = sscanf(val, "%lu", &v);
681 if (ret != 1) {
682 return 0;
683 } else {
684 return v;
685 }
686 }
687 }
688
689 void obsdb_close(ObsDB *db)
690 {
691 if (!db)
692 return;
693 rocksdb_mergeoperator_destroy(db->mergeop);
694 rocksdb_writeoptions_destroy(db->writeoptions);
695 rocksdb_readoptions_destroy(db->readoptions);
696 // rocksdb_options_destroy(db->options);
697 rocksdb_close(db->db);
698 }
0 /*
1 balboa
2 Copyright (c) 2018, DCSO GmbH
3 */
4
5 #ifndef OBS_ROCKSDB_H
6 #define OBS_ROCKSDB_H
7
8 #include <stdbool.h>
9 #include <stdint.h>
10 #include <time.h>
11
12 typedef struct Error Error;
13
14 Error* error_new();
15 const char* error_get(Error*);
16 bool error_is_set(Error*);
17 void error_delete(Error*);
18
19 typedef struct {
20 char *key,
21 *inv_key;
22 uint32_t count,
23 last_seen,
24 first_seen;
25 } Observation;
26
27 typedef struct ObsSet ObsSet;
28
29 unsigned long obs_set_size(ObsSet*);
30 const Observation* obs_set_get(ObsSet*, unsigned long);
31 void obs_set_delete(ObsSet*);
32
33 typedef struct ObsDB ObsDB;
34
35 ObsDB* obsdb_open(const char *path, size_t membudget, Error*);
36 ObsDB* obsdb_open_readonly(const char *path, Error*);
37 int obsdb_put(ObsDB *db, Observation *obs, Error*);
38 ObsSet* obsdb_search(ObsDB *db, const char *qrdata, const char *qrrname,
39 const char *qrrtype, const char *qsensorID);
40 int obsdb_dump(ObsDB *db, Error *e) ;
41
42 unsigned long obsdb_num_keys(ObsDB*);
43 void obsdb_close(ObsDB*);
44
45 #endif⏎
0 CREATE KEYSPACE IF NOT EXISTS balboa WITH REPLICATION = {
1 'class' : 'SimpleStrategy',
2 'replication_factor' : 2
3 };
4
5 CREATE TABLE balboa.observations_by_rdata (
6 rrname text,
7 rdata text,
8 rrtype text,
9 sensor_id text,
10 last_seen timestamp,
11 PRIMARY KEY (rdata, rrname, rrtype, sensor_id)
12 );
13 CREATE CUSTOM INDEX ON balboa.observations_by_rdata (rrname) USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = {
14 'analyzed' : 'true',
15 'analyzer_class' : 'org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer',
16 'case_sensitive' : 'false',
17 'mode' : 'CONTAINS'
18 };
19 ALTER TABLE balboa.observations_by_rdata
20 WITH COMPRESSION = {'sstable_compression': 'LZ4Compressor',
21 'chunk_length_kb': 64};
22
23 CREATE TABLE balboa.observations_by_rrname (
24 rrname text,
25 rdata text,
26 rrtype text,
27 sensor_id text,
28 last_seen timestamp,
29 PRIMARY KEY (rrname, rdata, rrtype, sensor_id)
30 );
31 CREATE CUSTOM INDEX ON balboa.observations_by_rrname (rdata) USING 'org.apache.cassandra.index.sasi.SASIIndex' WITH OPTIONS = {
32 'analyzed' : 'true',
33 'analyzer_class' : 'org.apache.cassandra.index.sasi.analyzer.NonTokenizingAnalyzer',
34 'case_sensitive' : 'false',
35 'mode' : 'CONTAINS'
36 };
37 ALTER TABLE balboa.observations_by_rrname
38 WITH COMPRESSION = {'sstable_compression': 'LZ4Compressor',
39 'chunk_length_kb': 64};
40
41 CREATE TABLE balboa.observations_counts (
42 rrname text,
43 rdata text,
44 rrtype text,
45 sensor_id text,
46 count counter,
47 PRIMARY KEY (rrname, rdata, rrtype, sensor_id)
48 );
49 ALTER TABLE balboa.observations_counts
50 WITH COMPRESSION = {'sstable_compression': 'LZ4Compressor',
51 'chunk_length_kb': 64};
52
53 CREATE TABLE balboa.observations_firstseen (
54 rrname text,
55 rdata text,
56 rrtype text,
57 sensor_id text,
58 first_seen timestamp,
59 PRIMARY KEY (rrname, rdata, rrtype, sensor_id)
60 );
61 ALTER TABLE balboa.observations_firstseen
62 WITH COMPRESSION = {'sstable_compression': 'LZ4Compressor',
63 'chunk_length_kb': 64};
0 input {
1 file {
2 path => "/var/log/passivedns.log"
3 }
4 }
5
6 output {
7 http {
8 http_method => 'post'
9 url => 'http://localhost:8081/submit'
10 format => "message"
11 message => "%{message}"
12 headers => ["X-Sensor-ID", "abcde"]
13 }
14 }
0 >>> /etc/packetbeat/packetbeat.yml
1 ----------------------------------
2
3 packetbeat.interfaces.device: any
4
5 packetbeat.protocols:
6 - type: dns
7 ports: [53]
8 include_authorities: true
9 include_additionals: true
10
11 output.logstash:
12 hosts: ["localhost:5044"]
13
14
15 >>> /etc/logstash/conf.d/pdns.conf
16 ----------------------------------
17
18 input {
19 beats {
20 port => 5044
21 }
22 }
23
24 filter {
25 mutate {
26 add_field => { "sensor_id" => "abcde" }
27 }
28 }
29
30 output {
31 http {
32 http_method => 'post'
33 url => 'http://localhost:8081/submit'
34 headers => ["X-Sensor-ID", "abcde"]
35 }
36 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package feeder
4
5 import (
6 "github.com/DCSO/balboa/format"
7 "github.com/DCSO/balboa/observation"
8 )
9
10 // Feeder is an interface of a component that accepts observations in a
11 // specific format and feeds them into a channel of InputObservations.
12 // An input decoder in the form of a MakeObservationFunc describes the
13 // operations necessary to transform the input format into an
14 // InputObservation.
15 type Feeder interface {
16 Run(chan observation.InputObservation) error
17 SetInputDecoder(format.MakeObservationFunc)
18 Stop(chan bool)
19 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package feeder
4
5 import (
6 "bytes"
7 "compress/gzip"
8 "fmt"
9 "io"
10 "strings"
11 "sync"
12 "time"
13
14 "github.com/DCSO/balboa/format"
15 "github.com/DCSO/balboa/observation"
16
17 "github.com/NeowayLabs/wabbit"
18 amqp "github.com/NeowayLabs/wabbit/amqp"
19 log "github.com/sirupsen/logrus"
20 origamqp "github.com/streadway/amqp"
21 )
22
23 // Consumer reads and processes messages from a fake RabbitMQ server.
24 type Consumer struct {
25 conn wabbit.Conn
26 channel wabbit.Channel
27 tag string
28 exchanges []string
29 URL string
30 done chan error
31 stop chan bool
32 deliveries <-chan wabbit.Delivery
33 Callback func(wabbit.Delivery)
34 StopReconnection chan bool
35 ChanMutex sync.Mutex
36 ConnMutex sync.Mutex
37 OutChan chan observation.InputObservation
38 MakeObservationFunc format.MakeObservationFunc
39 ErrorChan chan wabbit.Error
40 Reconnector func(string) (wabbit.Conn, string, error)
41 Connector func(*Consumer) error
42 }
43
44 func decompressBody(d wabbit.Delivery) ([]byte, error) {
45 var compressedBuffer bytes.Buffer
46 compressedBuffer.Write(d.Body())
47 compressedReader, err := gzip.NewReader(&compressedBuffer)
48 if err != nil {
49 return nil, err
50 }
51 decompressedWriter := new(bytes.Buffer)
52 io.Copy(decompressedWriter, compressedReader)
53 compressedReader.Close()
54 body := decompressedWriter.Bytes()
55 return body, nil
56 }
57
58 const amqpReconnDelay = 2 * time.Second
59
60 func reconnectOnFailure(s *Consumer) {
61 for {
62 select {
63 case <-s.StopReconnection:
64 return
65 case rabbitErr := <-s.ErrorChan:
66 if rabbitErr != nil {
67 log.Warnf("RabbitMQ connection failed: %s", rabbitErr.Reason())
68 close(s.stop)
69 for {
70 time.Sleep(amqpReconnDelay)
71 connErr := s.Connector(s)
72 if connErr != nil {
73 log.Warnf("RabbitMQ error: %s", connErr)
74 } else {
75 log.Infof("Reestablished connection to %s", s.URL)
76 s.ConnMutex.Lock()
77 s.conn.NotifyClose(s.ErrorChan)
78 s.ConnMutex.Unlock()
79 go handle(s.deliveries, s.stop, s.done, s.OutChan, s.MakeObservationFunc)
80 break
81 }
82 }
83 }
84 }
85 }
86 }
87
88 // NewConsumerWithReconnector creates a new consumer with the given properties. The callback
89 // function is called for each delivery accepted from a consumer channel.
90 func (f *AMQPFeeder) NewConsumerWithReconnector(amqpURI string, exchanges []string, exchangeType,
91 queueName, key, ctag string, out chan observation.InputObservation,
92 reconnector func(string) (wabbit.Conn, string, error)) (*Consumer, error) {
93 var err error
94 c := &Consumer{
95 conn: nil,
96 channel: nil,
97 exchanges: exchanges,
98 URL: amqpURI,
99 tag: ctag,
100 done: make(chan error),
101 stop: make(chan bool),
102 OutChan: out,
103 MakeObservationFunc: f.MakeObservationFunc,
104 Reconnector: reconnector,
105 StopReconnection: make(chan bool),
106 }
107
108 c.Connector = func(s *Consumer) error {
109 var err error
110 var exchangeType string
111
112 s.ConnMutex.Lock()
113 s.conn, exchangeType, err = s.Reconnector(s.URL)
114 s.ConnMutex.Unlock()
115 if err != nil {
116 return err
117 }
118 s.ChanMutex.Lock()
119 s.channel, err = s.conn.Channel()
120 s.ChanMutex.Unlock()
121 if err != nil {
122 s.ConnMutex.Lock()
123 s.conn.Close()
124 s.ConnMutex.Unlock()
125 return err
126 }
127
128 for _, exchange := range exchanges {
129 // We do not want to declare an exchange on non-default connection methods,
130 // as they may not support all exchange types. For instance amqptest does
131 // not support 'fanout'.
132 log.Debug("declaring exchange ", exchange)
133 err = s.channel.ExchangeDeclare(
134 exchange,
135 exchangeType,
136 wabbit.Option{
137 "durable": true,
138 "autoDelete": false,
139 "internal": false,
140 "noWait": false,
141 },
142 )
143 if err != nil {
144 log.Error(err)
145 s.ChanMutex.Lock()
146 s.channel.Close()
147 s.ChanMutex.Unlock()
148 s.ConnMutex.Lock()
149 s.conn.Close()
150 s.ConnMutex.Unlock()
151 return err
152 }
153 }
154 queueName := fmt.Sprintf("%s.%s", strings.Join(exchanges, "."), queueName)
155 queue, err := c.channel.QueueDeclare(
156 queueName,
157 wabbit.Option{
158 "durable": false,
159 "autoDelete": true,
160 "exclusive": true,
161 "noWait": false,
162 "args": origamqp.Table{
163 "x-message-ttl": int32(300000),
164 "x-max-length-bytes": int32(100 * 1024 * 1024),
165 },
166 },
167 )
168 if err != nil {
169 return fmt.Errorf("Queue Declare: %s", err)
170 }
171 log.Debugf("declared Queue (%q %d messages, %d consumers), binding to Exchange (key %q)",
172 queue.Name(), queue.Messages(), queue.Consumers(), key)
173
174 for _, exchange := range exchanges {
175 log.Debug("binding to exchange ", exchange)
176 if err = c.channel.QueueBind(
177 queue.Name(),
178 key,
179 exchange,
180 wabbit.Option{
181 "noWait": false,
182 },
183 ); err != nil {
184 return fmt.Errorf("Queue Bind: %s", err)
185 }
186 }
187
188 log.Debugf("Queue bound to Exchange, starting Consume (consumer tag %q)", c.tag)
189 c.deliveries, err = c.channel.Consume(
190 queue.Name(),
191 c.tag,
192 wabbit.Option{
193 "autoAck": false,
194 "exclusive": false,
195 "noLocal": false,
196 "noWait": false,
197 },
198 )
199 if err != nil {
200 return fmt.Errorf("Queue Consume: %s", err)
201 }
202
203 log.Debugf("Consumer established connection to %s", s.URL)
204 s.stop = make(chan bool)
205 c.ErrorChan = make(chan wabbit.Error)
206
207 return nil
208 }
209
210 c.ErrorChan = make(chan wabbit.Error)
211 err = c.Connector(c)
212 if err != nil {
213 return nil, err
214 }
215 c.conn.NotifyClose(c.ErrorChan)
216
217 go reconnectOnFailure(c)
218 go handle(c.deliveries, c.stop, c.done, out, f.MakeObservationFunc)
219
220 return c, nil
221 }
222
223 func defaultReconnector(amqpURI string) (wabbit.Conn, string, error) {
224 conn, err := amqp.Dial(amqpURI)
225 if err != nil {
226 return nil, "fanout", err
227 }
228 return conn, "fanout", err
229 }
230
231 // NewConsumer returns a new Consumer.
232 func (f *AMQPFeeder) NewConsumer(amqpURI string, exchanges []string, exchangeType, queueName, key,
233 ctag string, out chan observation.InputObservation) (*Consumer, error) {
234 return f.NewConsumerWithReconnector(amqpURI, exchanges, exchangeType, queueName, key,
235 ctag, out, defaultReconnector)
236 }
237
238 // Shutdown shuts down a consumer, closing down its channels and connections.
239 func (c *Consumer) Shutdown() error {
240 // will close() the deliveries channel
241 if err := c.channel.Close(); err != nil {
242 return fmt.Errorf("Channel close failed: %s", err)
243 }
244 if err := c.conn.Close(); err != nil {
245 return fmt.Errorf("AMQP connection close error: %s", err)
246 }
247 defer log.Debugf("AMQP shutdown OK")
248 // wait for handle() to exit
249 return <-c.done
250 }
251
252 func handle(deliveries <-chan wabbit.Delivery, stop chan bool, done chan error,
253 out chan observation.InputObservation, fn format.MakeObservationFunc) {
254 for {
255 select {
256 case <-stop:
257 done <- nil
258 return
259 case d := <-deliveries:
260 if d == nil {
261 done <- nil
262 return
263 }
264 log.Infof("got %d bytes via AMQP", len(d.Body()))
265 raw := d.Body()
266 if _, ok := d.Headers()["compressed"]; ok {
267 var err error
268 raw, err = decompressBody(d)
269 if err != nil {
270 log.Warn(err)
271 continue
272 }
273 }
274 var sensorID string
275 if _, ok := d.Headers()["sensor_id"]; ok {
276 sensorID = d.Headers()["sensor_id"].(string)
277 }
278 err := fn(raw, sensorID, out, stop)
279 if err != nil {
280 log.Warn(err)
281 }
282 d.Ack(true)
283 }
284 }
285
286 }
287
288 // AMQPFeeder is a Feeder that accepts input via AMQP queues.
289 type AMQPFeeder struct {
290 StopChan chan bool
291 StoppedChan chan bool
292 IsRunning bool
293 Consumer *Consumer
294 URL string
295 Exchanges []string
296 Queue string
297 MakeObservationFunc format.MakeObservationFunc
298 }
299
300 // MakeAMQPFeeder returns a new AMQPFeeder, connecting to the AMQP server at
301 // the given URL, creating a new queue with the given name bound to the
302 // provided exchanges.
303 func MakeAMQPFeeder(url string, exchanges []string, queue string) *AMQPFeeder {
304 return &AMQPFeeder{
305 IsRunning: false,
306 StopChan: make(chan bool),
307 Exchanges: exchanges,
308 URL: url,
309 Queue: queue,
310 MakeObservationFunc: format.MakeFeverAggregateInputObservations,
311 }
312 }
313
314 // SetInputDecoder states that the given MakeObservationFunc should be used to
315 // parse and decode data delivered to this feeder.
316 func (f *AMQPFeeder) SetInputDecoder(fn format.MakeObservationFunc) {
317 f.MakeObservationFunc = fn
318 }
319
320 // Run starts the feeder.
321 func (f *AMQPFeeder) Run(out chan observation.InputObservation) error {
322 var err error
323 f.Consumer, err = f.NewConsumer(f.URL, f.Exchanges, "fanout", f.Queue, "", "balboa", out)
324 if err != nil {
325 return err
326 }
327 f.IsRunning = true
328 return nil
329 }
330
331 // Stop causes the feeder to stop accepting deliveries and close all
332 // associated channels, including the passed notification channel.
333 func (f *AMQPFeeder) Stop(stopChan chan bool) {
334 close(stopChan)
335 if f.IsRunning {
336 f.Consumer.Shutdown()
337 }
338 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package feeder
4
5 import (
6 "fmt"
7 "strings"
8
9 "github.com/DCSO/balboa/format"
10 "github.com/DCSO/balboa/observation"
11
12 log "github.com/sirupsen/logrus"
13 yaml "gopkg.in/yaml.v2"
14 )
15
16 // Setup describes a collection of feeders that should be active, including
17 // their configuration settings.
18 type Setup struct {
19 Feeder []struct {
20 Name string `yaml:"name"`
21 Type string `yaml:"type"`
22 InputFormat string `yaml:"input_format"`
23 // for AMQP
24 URL string `yaml:"url"`
25 Exchange []string `yaml:"exchange"`
26 // for HTTP etc.
27 ListenHost string `yaml:"listen_host"`
28 ListenPort int `yaml:"listen_port"`
29 // for socket input
30 Path string `yaml:"path"`
31 } `yaml:"feeder"`
32 Feeders map[string]Feeder
33 }
34
35 // LoadSetup creates a new Setup from a byte array containing YAML.
36 func LoadSetup(in []byte) (*Setup, error) {
37 var fs Setup
38 seenFeeders := make(map[string]bool)
39 err := yaml.Unmarshal(in, &fs)
40 if err != nil {
41 return nil, err
42 }
43 fs.Feeders = make(map[string]Feeder)
44 for _, f := range fs.Feeder {
45 if f.Name == "" {
46 return nil, fmt.Errorf("name missing")
47 }
48 if _, ok := seenFeeders[f.Name]; ok {
49 return nil, fmt.Errorf("duplicate name: %s", f.Name)
50 }
51 seenFeeders[f.Name] = true
52 if f.Type == "" {
53 return nil, fmt.Errorf("type missing")
54 }
55 if f.InputFormat == "" {
56 return nil, fmt.Errorf("input format missing")
57 }
58 switch f.Type {
59 case "amqp":
60 if len(f.Exchange) == 0 {
61 return nil, fmt.Errorf("%s: Exchange missing", f.Name)
62 }
63 if f.URL == "" {
64 return nil, fmt.Errorf("%s: URL missing", f.Name)
65 }
66 case "http":
67 if f.ListenHost == "" {
68 return nil, fmt.Errorf("%s: ListenHost missing", f.Name)
69 }
70 if f.ListenPort == 0 {
71 return nil, fmt.Errorf("%s: ListenPort missing", f.Name)
72 }
73 case "socket":
74 if f.Path == "" {
75 return nil, fmt.Errorf("%s: socket Path missing", f.Name)
76 }
77 }
78 }
79 return &fs, nil
80 }
81
82 // Run starts all feeders according to the description in the setup, in the
83 // background. Use Stop() to stop the feeders.
84 func (fs *Setup) Run(in chan observation.InputObservation) error {
85 for _, v := range fs.Feeder {
86 log.Infof("starting feeder %s", v.Name)
87 switch v.Type {
88 case "amqp":
89 queueName := strings.ToLower(strings.Replace(v.Name, " ", "_", -1))
90 fs.Feeders[v.Name] = MakeAMQPFeeder(v.URL, v.Exchange, queueName)
91 fs.Feeders[v.Name].Run(in)
92 case "http":
93 fs.Feeders[v.Name] = MakeHTTPFeeder(v.ListenHost, v.ListenPort)
94 fs.Feeders[v.Name].Run(in)
95 case "socket":
96 f, err := MakeSocketFeeder(v.Path)
97 if err != nil {
98 return err
99 }
100 fs.Feeders[v.Name] = f
101 fs.Feeders[v.Name].Run(in)
102 }
103 switch v.InputFormat {
104 case "fever_aggregate":
105 fs.Feeders[v.Name].SetInputDecoder(format.MakeFeverAggregateInputObservations)
106 case "gopassivedns":
107 fs.Feeders[v.Name].SetInputDecoder(format.MakeGopassivednsInputObservations)
108 case "packetbeat":
109 fs.Feeders[v.Name].SetInputDecoder(format.MakePacketbeatInputObservations)
110 case "suricata_dns":
111 fs.Feeders[v.Name].SetInputDecoder(format.MakeSuricataInputObservations)
112 case "gamelinux":
113 fs.Feeders[v.Name].SetInputDecoder(format.MakeFjellskaalInputObservations)
114 default:
115 log.Fatalf("unknown input format: %s", v.InputFormat)
116 }
117 }
118 return nil
119 }
120
121 // Stop causes all feeders described in the setup to cease processing input.
122 // The stopChan will be closed once all feeders are done shutting down.
123 func (fs *Setup) Stop(stopChan chan bool) {
124 for k, v := range fs.Feeders {
125 log.Infof("stopping feeder %s", k)
126 myStopChan := make(chan bool)
127 v.Stop(myStopChan)
128 <-myStopChan
129 log.Infof("feeder %s stopped", k)
130 }
131 close(stopChan)
132 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package feeder
4
5 import (
6 "context"
7 "fmt"
8 "io/ioutil"
9 "net/http"
10
11 "github.com/DCSO/balboa/format"
12 "github.com/DCSO/balboa/observation"
13
14 log "github.com/sirupsen/logrus"
15 )
16
17 // HTTPFeeder is a Feeder implementation that accepts HTTP requests to obtain
18 // observations.
19 type HTTPFeeder struct {
20 StopChan chan bool
21 StoppedChan chan bool
22 IsRunning bool
23 Port int
24 Host string
25 MakeObservationFunc format.MakeObservationFunc
26 Server *http.Server
27 OutChan chan observation.InputObservation
28 }
29
30 // MakeHTTPFeeder creates a new HTTPFeeder listening on a specific address
31 // and port.
32 func MakeHTTPFeeder(host string, port int) *HTTPFeeder {
33 return &HTTPFeeder{
34 IsRunning: false,
35 StopChan: make(chan bool),
36 Port: port,
37 Host: host,
38 MakeObservationFunc: format.MakeFeverAggregateInputObservations,
39 }
40 }
41
42 // SetInputDecoder states that the given MakeObservationFunc should be used to
43 // parse and decode data delivered to this feeder.
44 func (f *HTTPFeeder) SetInputDecoder(fn format.MakeObservationFunc) {
45 f.MakeObservationFunc = fn
46 }
47
48 func (f *HTTPFeeder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
49 sensorID := r.Header.Get("X-Sensor-ID")
50 if r.Method == http.MethodPost {
51 body, err := ioutil.ReadAll(r.Body)
52 log.Infof("got %d bytes via HTTP", len(body))
53 if err != nil {
54 log.Warn(err)
55 return
56 }
57 f.MakeObservationFunc(body, sensorID, f.OutChan, f.StopChan)
58 }
59 w.WriteHeader(200)
60 }
61
62 // Run starts the feeder.
63 func (f *HTTPFeeder) Run(out chan observation.InputObservation) error {
64 f.OutChan = out
65 f.Server = &http.Server{
66 Addr: fmt.Sprintf("%s:%d", f.Host, f.Port),
67 Handler: f,
68 }
69 log.Infof("accepting submissions on port %v", f.Port)
70 go func() {
71 err := f.Server.ListenAndServe()
72 if err != nil {
73 log.Info(err)
74 }
75 }()
76 f.IsRunning = true
77 return nil
78 }
79
80 // Stop causes the feeder to stop accepting requests and close all
81 // associated channels, including the passed notification channel.
82 func (f *HTTPFeeder) Stop(stopChan chan bool) {
83 if f.IsRunning {
84 f.Server.Shutdown(context.TODO())
85 }
86 close(stopChan)
87 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package feeder
4
5 import (
6 "bufio"
7 "net"
8 "time"
9
10 "github.com/DCSO/balboa/format"
11 "github.com/DCSO/balboa/observation"
12
13 log "github.com/sirupsen/logrus"
14 )
15
16 // SocketFeeder is a Feeder implementation that reds data from a UNIX socket.
17 type SocketFeeder struct {
18 ObsChan chan observation.InputObservation
19 Verbose bool
20 Running bool
21 InputListener net.Listener
22 MakeObservationFunc format.MakeObservationFunc
23 StopChan chan bool
24 StoppedChan chan bool
25 }
26
27 func (sf *SocketFeeder) handleServerConnection() {
28 for {
29 select {
30 case <-sf.StopChan:
31 sf.InputListener.Close()
32 close(sf.StoppedChan)
33 return
34 default:
35 sf.InputListener.(*net.UnixListener).SetDeadline(time.Now().Add(1e9))
36 c, err := sf.InputListener.Accept()
37 if nil != err {
38 if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
39 continue
40 }
41 log.Info(err)
42 }
43
44 scanner := bufio.NewScanner(c)
45 buf := make([]byte, 0, 32*1024*1024)
46 scanner.Buffer(buf, 32*1024*1024)
47 for {
48 for scanner.Scan() {
49 select {
50 case <-sf.StopChan:
51 sf.InputListener.Close()
52 close(sf.StoppedChan)
53 return
54 default:
55 json := scanner.Bytes()
56 sf.MakeObservationFunc(json, "[unknown]", sf.ObsChan, sf.StopChan)
57 }
58 }
59 errRead := scanner.Err()
60 if errRead == nil {
61 break
62 } else if errRead == bufio.ErrTooLong {
63 log.Warn(errRead)
64 scanner = bufio.NewScanner(c)
65 scanner.Buffer(buf, 2*cap(buf))
66 } else {
67 log.Warn(errRead)
68 }
69 }
70 }
71 }
72 }
73
74 // MakeSocketFeeder returns a new SocketFeeder reading from the Unix socket
75 // inputSocket and writing parsed events to outChan. If no such socket could be
76 // created for listening, the error returned is set accordingly.
77 func MakeSocketFeeder(inputSocket string) (*SocketFeeder, error) {
78 var err error
79 si := &SocketFeeder{
80 Verbose: false,
81 StopChan: make(chan bool),
82 }
83 si.InputListener, err = net.Listen("unix", inputSocket)
84 if err != nil {
85 return nil, err
86 }
87 return si, err
88 }
89
90 // SetInputDecoder states that the given MakeObservationFunc should be used to
91 // parse and decode data delivered to this feeder.
92 func (sf *SocketFeeder) SetInputDecoder(fn format.MakeObservationFunc) {
93 sf.MakeObservationFunc = fn
94 }
95
96 // Run starts the feeder.
97 func (sf *SocketFeeder) Run(out chan observation.InputObservation) error {
98 if !sf.Running {
99 sf.ObsChan = out
100 sf.Running = true
101 sf.StopChan = make(chan bool)
102 go sf.handleServerConnection()
103 }
104 return nil
105 }
106
107 // Stop causes the SocketFeeder to stop reading from the socket and close all
108 // associated channels, including the passed notification channel.
109 func (sf *SocketFeeder) Stop(stoppedChan chan bool) {
110 if sf.Running {
111 sf.StoppedChan = stoppedChan
112 close(sf.StopChan)
113 sf.Running = false
114 }
115 }
0 feeder:
1 - name: AMQPInput2
2 type: amqp
3 url: amqp://guest:guest@localhost:5672
4 exchange: [ tdh.pdns ]
5 input_format: fever_aggregate
6 - name: HTTP Input
7 type: http
8 listen_host: 127.0.0.1
9 listen_port: 8081
10 input_format: fever_aggregate
11 - name: Socket Input
12 type: socket
13 path: /tmp/balboa.sock
14 input_format: fever_aggregate
15 - name: Suricata Socket Input
16 type: socket
17 path: /tmp/suri.sock
18 input_format: suricata_dns
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package format
4
5 import "github.com/DCSO/balboa/observation"
6
7 // MakeObservationFunc is a function that accepts a byte array with input
8 // obtained from a feeder, a sensor ID, a channel for the generated
9 // InputObservations, and a channel to signal a stop.
10 type MakeObservationFunc func([]byte, string, chan observation.InputObservation, chan bool) error
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package format
4
5 import (
6 "encoding/json"
7 "time"
8
9 "github.com/DCSO/balboa/observation"
10
11 log "github.com/sirupsen/logrus"
12 )
13
14 type rdata struct {
15 AnsweringHost string `json:"answering_host"`
16 Count int `json:"count"`
17 Rcode string `json:"rcode"`
18 Rdata string `json:"rdata"`
19 Rrtype string `json:"rrtype"`
20 Type string `json:"type"`
21 }
22
23 type inputJSONstruct struct {
24 DNS map[string]struct {
25 Rdata []rdata `json:"rdata"`
26 } `json:"dns"`
27 TimestampEnd time.Time `json:"timestamp_end"`
28 TimestampStart time.Time `json:"timestamp_start"`
29 }
30
31 // MakeFeverAggregateInputObservations is a MakeObservationFunc that accepts
32 // input in suristasher/FEVER's JSON format.
33 func MakeFeverAggregateInputObservations(inputJSON []byte, sensorID string, out chan observation.InputObservation, stop chan bool) error {
34 var in inputJSONstruct
35 var i int64
36 err := json.Unmarshal(inputJSON, &in)
37 if err != nil {
38 log.Warn(err)
39 return nil
40 }
41 for k, v := range in.DNS {
42 select {
43 case <-stop:
44 return nil
45 default:
46 for _, v2 := range v.Rdata {
47 select {
48 case <-stop:
49 return nil
50 default:
51 o := observation.InputObservation{
52 Count: v2.Count,
53 Rdata: v2.Rdata,
54 Rrname: k,
55 Rrtype: v2.Rrtype,
56 SensorID: sensorID,
57 TimestampEnd: in.TimestampEnd,
58 TimestampStart: in.TimestampStart,
59 }
60 i++
61 out <- o
62 }
63 }
64 }
65 }
66 log.Infof("enqueued %d observations", i)
67 return nil
68 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package format
4
5 import (
6 "testing"
7 "time"
8
9 "github.com/DCSO/balboa/observation"
10 "github.com/sirupsen/logrus/hooks/test"
11 )
12
13 func TestFeverFormatFail(t *testing.T) {
14 hook := test.NewGlobal()
15
16 resultObs := make([]observation.InputObservation, 0)
17 resChan := make(chan observation.InputObservation)
18 go func() {
19 for o := range resChan {
20 resultObs = append(resultObs, o)
21 }
22 }()
23
24 stopChan := make(chan bool)
25 err := MakeFeverAggregateInputObservations([]byte(`babanana`), "foo", resChan, stopChan)
26 if err != nil {
27 t.Fatal(err)
28 }
29 close(resChan)
30 close(stopChan)
31
32 if len(resultObs) != 0 {
33 t.Fail()
34 }
35 if len(hook.Entries) != 1 {
36 t.Fail()
37 }
38 }
39
40 func TestFeverFormatEmpty(t *testing.T) {
41 hook := test.NewGlobal()
42
43 resultObs := make([]observation.InputObservation, 0)
44 resChan := make(chan observation.InputObservation)
45 go func() {
46 for o := range resChan {
47 resultObs = append(resultObs, o)
48 }
49 }()
50
51 stopChan := make(chan bool)
52 err := MakeFeverAggregateInputObservations([]byte(""), "foo", resChan, stopChan)
53 if err != nil {
54 t.Fatal(err)
55 }
56 close(resChan)
57 close(stopChan)
58
59 if len(resultObs) != 0 {
60 t.Fail()
61 }
62 if len(hook.Entries) != 1 {
63 t.Fail()
64 }
65 }
66
67 const exampleInFever = `{
68 "dns": {
69 "foo.bar": {
70 "rdata": [
71 {
72 "rdata": "1.2.3.4",
73 "count":2,
74 "rrtype": "A",
75 "type":"answer"
76 },
77 {
78 "rdata": "1.2.3.5",
79 "count":1,
80 "rrtype": "A",
81 "type":"answer"
82 }
83 ]
84 }
85 },
86 "timestamp_start":"2018-10-26T21:02:20+00:00",
87 "timestamp_end":"2018-10-26T21:03:20+00:00"
88 }`
89
90 func TestFeverFormat(t *testing.T) {
91 hook := test.NewGlobal()
92
93 resultObs := make([]observation.InputObservation, 0)
94 resChan := make(chan observation.InputObservation)
95 go func() {
96 for o := range resChan {
97 resultObs = append(resultObs, o)
98 }
99 }()
100
101 stopChan := make(chan bool)
102 err := MakeFeverAggregateInputObservations([]byte(exampleInFever), "foo", resChan, stopChan)
103 if err != nil {
104 t.Fatal(err)
105 }
106 time.Sleep(500 * time.Millisecond)
107 close(resChan)
108 close(stopChan)
109
110 if len(hook.Entries) != 1 {
111 t.Fail()
112 }
113 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package format
4
5 import (
6 "bufio"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/DCSO/balboa/observation"
12
13 log "github.com/sirupsen/logrus"
14 )
15
16 // MakeFjellskaalInputObservations is a MakeObservationFunc that consumes
17 // input in the format as used by https://github.com/gamelinux/passivedns.
18 func MakeFjellskaalInputObservations(inputJSON []byte, sensorID string, out chan observation.InputObservation, stop chan bool) error {
19 var i int
20 scanner := bufio.NewScanner(strings.NewReader(string(inputJSON)))
21 for scanner.Scan() {
22 select {
23 case <-stop:
24 return nil
25 default:
26 vals := strings.Split(scanner.Text(), "||")
27 if len(vals) == 9 {
28 times := strings.Split(vals[0], ".")
29 if len(times) != 2 {
30 log.Warn("timestamp does not have form X.X")
31 continue
32 }
33 epoch, err := strconv.Atoi(times[0])
34 if err != nil {
35 log.Warn(err)
36 continue
37 }
38 nsec, err := strconv.Atoi(times[1])
39 if err != nil {
40 log.Warn(err)
41 continue
42 }
43 timestamp := time.Unix(int64(epoch), int64(nsec))
44 rrname := vals[4]
45 rrtype := vals[5]
46 rdata := vals[6]
47 count, err := strconv.Atoi(vals[8])
48 if err != nil {
49 log.Warn(err)
50 continue
51 }
52 o := observation.InputObservation{
53 Count: count,
54 Rdata: strings.TrimRight(rdata, "."),
55 Rrname: strings.TrimRight(rrname, "."),
56 Rrtype: rrtype,
57 SensorID: sensorID,
58 TimestampEnd: timestamp,
59 TimestampStart: timestamp,
60 }
61 i++
62 out <- o
63 } else {
64 log.Warn("number of columns != 9")
65 }
66 }
67 }
68
69 log.Debugf("enqueued %d observations", i)
70 return nil
71 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package format
4
5 import (
6 "testing"
7 "time"
8
9 "github.com/DCSO/balboa/observation"
10 "github.com/sirupsen/logrus/hooks/test"
11 )
12
13 func TestFjellskaalFormatFail(t *testing.T) {
14 hook := test.NewGlobal()
15
16 resultObs := make([]observation.InputObservation, 0)
17 resChan := make(chan observation.InputObservation)
18 go func() {
19 for o := range resChan {
20 resultObs = append(resultObs, o)
21 }
22 }()
23
24 stopChan := make(chan bool)
25 err := MakeFjellskaalInputObservations([]byte("1322849924||10.1.1.1||8.8.8.8||IN||upload.youtube.com.||A||74.125.43.117||46587||5"), "foo", resChan, stopChan)
26 if err != nil {
27 t.Fatal(err)
28 }
29 err = MakeFjellskaalInputObservations([]byte("1322849924||10.1.1.1||8.8.8.8||upload.youtube.com.||A||74.125.43.117||46587||5"), "foo", resChan, stopChan)
30 if err != nil {
31 t.Fatal(err)
32 }
33 err = MakeFjellskaalInputObservations([]byte("X.2332||10.1.1.1||8.8.8.8||IN||upload.youtube.com.||A||74.125.43.117||46587||5"), "foo", resChan, stopChan)
34 if err != nil {
35 t.Fatal(err)
36 }
37 err = MakeFjellskaalInputObservations([]byte("X.X||10.1.1.1||8.8.8.8||IN||upload.youtube.com.||A||74.125.43.117||46587||5"), "foo", resChan, stopChan)
38 if err != nil {
39 t.Fatal(err)
40 }
41 err = MakeFjellskaalInputObservations([]byte("23232.X||10.1.1.1||8.8.8.8||IN||upload.youtube.com.||A||74.125.43.117||46587||5"), "foo", resChan, stopChan)
42 if err != nil {
43 t.Fatal(err)
44 }
45 err = MakeFjellskaalInputObservations([]byte("1322849924.244555||10.1.1.1||8.8.8.8||IN||upload.youtube.com.||A||74.125.43.117||46587||bar"), "foo", resChan, stopChan)
46 if err != nil {
47 t.Fatal(err)
48 }
49 close(resChan)
50 close(stopChan)
51
52 if len(resultObs) != 0 {
53 t.Fail()
54 }
55 if len(hook.Entries) != 6 {
56 t.Fail()
57 }
58 }
59
60 func TestFjellskaalFormatEmpty(t *testing.T) {
61 hook := test.NewGlobal()
62
63 resultObs := make([]observation.InputObservation, 0)
64 resChan := make(chan observation.InputObservation)
65 go func() {
66 for o := range resChan {
67 resultObs = append(resultObs, o)
68 }
69 }()
70
71 stopChan := make(chan bool)
72 err := MakeFjellskaalInputObservations([]byte(""), "foo", resChan, stopChan)
73 if err != nil {
74 t.Fatal(err)
75 }
76 close(resChan)
77 close(stopChan)
78
79 if len(resultObs) != 0 {
80 t.Fail()
81 }
82 if len(hook.Entries) != 0 {
83 t.Fail()
84 }
85 }
86
87 const exampleInFjellskaal = `1322849924.408856||10.1.1.1||8.8.8.8||IN||upload.youtube.com.||A||74.125.43.117||46587||5
88 1322849924.408857||10.1.1.1||8.8.8.8||IN||upload.youtube.com.||A||74.125.43.116||420509||5
89 1322849924.408858||10.1.1.1||8.8.8.8||IN||www.adobe.com.||CNAME||www.wip4.adobe.com.||43200||8
90 1322849924.408859||10.1.1.1||8.8.8.8||IN||www.adobe.com.||A||193.104.215.61||43200||8
91 1322849924.408860||10.1.1.1||8.8.8.8||IN||i1.ytimg.com.||CNAME||ytimg.l.google.com.||43200||3
92 1322849924.408861||10.1.1.1||8.8.8.8||IN||clients1.google.com.||A||173.194.32.3||43200||2
93 `
94
95 func TestFjellskaalFormat(t *testing.T) {
96 hook := test.NewGlobal()
97
98 resultObs := make([]observation.InputObservation, 0)
99 resChan := make(chan observation.InputObservation)
100 go func() {
101 for o := range resChan {
102 resultObs = append(resultObs, o)
103 }
104 }()
105
106 stopChan := make(chan bool)
107 err := MakeFjellskaalInputObservations([]byte(exampleInFjellskaal), "foo", resChan, stopChan)
108 if err != nil {
109 t.Fatal(err)
110 }
111 time.Sleep(500 * time.Millisecond)
112 close(resChan)
113 close(stopChan)
114
115 if len(hook.Entries) != 0 {
116 t.Fail()
117 }
118 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package format
4
5 import (
6 "encoding/json"
7 "net"
8 "time"
9
10 "github.com/DCSO/balboa/observation"
11
12 log "github.com/sirupsen/logrus"
13 )
14
15 type dnsLogEntry struct {
16 QueryID uint16 `json:"query_id"`
17 ResponseCode int `json:"rcode"`
18 Question string `json:"q"`
19 QuestionType string `json:"qtype"`
20 Answer string `json:"a"`
21 AnswerType string `json:"atype"`
22 TTL uint32 `json:"ttl"`
23 Server net.IP `json:"dst"`
24 Client net.IP `json:"src"`
25 Timestamp string `json:"tstamp"`
26 Elapsed int64 `json:"elapsed"`
27 ClientPort string `json:"sport"`
28 Level string `json:"level"`
29 Length int `json:"bytes"`
30 Proto string `json:"protocol"`
31 Truncated bool `json:"truncated"`
32 AuthoritativeAnswer bool `json:"aa"`
33 RecursionDesired bool `json:"rd"`
34 RecursionAvailable bool `json:"ra"`
35 }
36
37 // MakeGopassivednsInputObservations is a MakeObservationFunc that accepts
38 // input in the format as generated by https://github.com/Phillipmartin/gopassivedns.
39 func MakeGopassivednsInputObservations(inputJSON []byte, sensorID string, out chan observation.InputObservation, stop chan bool) error {
40 var in dnsLogEntry
41 err := json.Unmarshal(inputJSON, &in)
42 if err != nil {
43 log.Warn(err)
44 return nil
45 }
46 tst, err := time.Parse("2006-01-02 15:04:05.999999 -0700 MST", in.Timestamp)
47 if err != nil {
48 log.Info(err)
49 return nil
50 }
51 o := observation.InputObservation{
52 Count: 1,
53 Rdata: in.Answer,
54 Rrname: in.Question,
55 Rrtype: in.AnswerType,
56 SensorID: sensorID,
57 TimestampEnd: tst,
58 TimestampStart: tst,
59 }
60 out <- o
61 log.Debug("enqueued 1 observation")
62 return nil
63 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package format
4
5 import (
6 "testing"
7 "time"
8
9 "github.com/DCSO/balboa/observation"
10 "github.com/sirupsen/logrus/hooks/test"
11 )
12
13 func TestGopassivednsFormatFail(t *testing.T) {
14 hook := test.NewGlobal()
15
16 resultObs := make([]observation.InputObservation, 0)
17 resChan := make(chan observation.InputObservation)
18 go func() {
19 for o := range resChan {
20 resultObs = append(resultObs, o)
21 }
22 }()
23
24 stopChan := make(chan bool)
25 err := MakeGopassivednsInputObservations([]byte(`{"query_id":43264,"rcode":0,"q":"github.com","qtype":"A","a":"192.30.253.112","atype":"A","ttl":60,"dst":"9.9.9.9","src":"192.168.1.79","tstamp":"2018-10-26 19 +0000 UTC","elapsed":35879000,"sport":"40651","level":"","bytes":102,"protocol":"udp","truncated":false,"aa":false,"rd":true,"ra":false}`), "foo", resChan, stopChan)
26 if err != nil {
27 t.Fatal(err)
28 }
29 close(resChan)
30 close(stopChan)
31
32 if len(resultObs) != 0 {
33 t.Fail()
34 }
35 if len(hook.Entries) != 1 {
36 t.Fail()
37 }
38 }
39
40 func TestGopassivednsFormatEmpty(t *testing.T) {
41 hook := test.NewGlobal()
42
43 resultObs := make([]observation.InputObservation, 0)
44 resChan := make(chan observation.InputObservation)
45 go func() {
46 for o := range resChan {
47 resultObs = append(resultObs, o)
48 }
49 }()
50
51 stopChan := make(chan bool)
52 err := MakeGopassivednsInputObservations([]byte(""), "foo", resChan, stopChan)
53 if err != nil {
54 t.Fatal(err)
55 }
56 close(resChan)
57 close(stopChan)
58
59 if len(resultObs) != 0 {
60 t.Fail()
61 }
62 if len(hook.Entries) != 1 {
63 t.Fail()
64 }
65 }
66
67 const exampleInGopassivedns = `{"query_id":43264,"rcode":0,"q":"github.com","qtype":"A","a":"192.30.253.112","atype":"A","ttl":60,"dst":"9.9.9.9","src":"192.168.1.79","tstamp":"2018-10-26 19:32:36.141184 +0000 UTC","elapsed":35879000,"sport":"40651","level":"","bytes":102,"protocol":"udp","truncated":false,"aa":false,"rd":true,"ra":false}
68 `
69
70 func TestGopassivednsFormat(t *testing.T) {
71 hook := test.NewGlobal()
72
73 resultObs := make([]observation.InputObservation, 0)
74 resChan := make(chan observation.InputObservation)
75 go func() {
76 for o := range resChan {
77 resultObs = append(resultObs, o)
78 }
79 }()
80
81 stopChan := make(chan bool)
82 err := MakeGopassivednsInputObservations([]byte(exampleInGopassivedns), "foo", resChan, stopChan)
83 if err != nil {
84 t.Fatal(err)
85 }
86 time.Sleep(500 * time.Millisecond)
87 close(resChan)
88 close(stopChan)
89
90 if len(hook.Entries) != 0 {
91 t.Fail()
92 }
93 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package format
4
5 import (
6 "encoding/json"
7 "strings"
8 "time"
9
10 "github.com/DCSO/balboa/observation"
11
12 log "github.com/sirupsen/logrus"
13 )
14
15 type pbEntry struct {
16 Type string `json:"type"`
17 Timestamp string `json:"@timestamp"`
18 DNS struct {
19 Answers []struct {
20 Name string `json:"name"`
21 Class string `json:"class"`
22 Type string `json:"type"`
23 Data string `json:"data"`
24 TTL string `json:"ttl"`
25 } `json:"answers"`
26 } `json:"dns"`
27 }
28
29 // MakePacketbeatInputObservations is a MakeObservationFunc that accepts a
30 // JSON format from Packetbeat via Logstash. See doc/packetbeat_config.txt
31 // for more information.
32 func MakePacketbeatInputObservations(inputJSON []byte, sensorID string, out chan observation.InputObservation, stop chan bool) error {
33 var in pbEntry
34 var i int
35 err := json.Unmarshal(inputJSON, &in)
36 if err != nil {
37 log.Warn(err)
38 return nil
39 }
40 if in.Type != "dns" {
41 return nil
42 }
43 tst, err := time.Parse("2006-01-02T15:04:05.999Z07", in.Timestamp)
44 if err != nil {
45 log.Warn(err)
46 return nil
47 }
48 for _, answer := range in.DNS.Answers {
49 select {
50 case <-stop:
51 return nil
52 default:
53 o := observation.InputObservation{
54 Count: 1,
55 Rdata: strings.TrimRight(answer.Data, "."),
56 Rrname: strings.TrimRight(answer.Name, "."),
57 Rrtype: answer.Type,
58 SensorID: sensorID,
59 TimestampEnd: tst,
60 TimestampStart: tst,
61 }
62 i++
63 out <- o
64 }
65 }
66 log.Infof("enqueued %d observations", i)
67 return nil
68 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package format
4
5 import (
6 "testing"
7 "time"
8
9 "github.com/DCSO/balboa/observation"
10 "github.com/sirupsen/logrus/hooks/test"
11 )
12
13 func TestPacketbeatFormatFail(t *testing.T) {
14 hook := test.NewGlobal()
15
16 resultObs := make([]observation.InputObservation, 0)
17 resChan := make(chan observation.InputObservation)
18 go func() {
19 for o := range resChan {
20 resultObs = append(resultObs, o)
21 }
22 }()
23
24 stopChan := make(chan bool)
25 err := MakePacketbeatInputObservations([]byte(`babanana`), "foo", resChan, stopChan)
26 if err != nil {
27 t.Fatal(err)
28 }
29 err = MakePacketbeatInputObservations([]byte(exampleInPacketbeatInvalidTimestamp), "foo", resChan, stopChan)
30 if err != nil {
31 t.Fatal(err)
32 }
33 err = MakePacketbeatInputObservations([]byte(exampleInPacketbeatInvalidType), "foo", resChan, stopChan)
34 if err != nil {
35 t.Fatal(err)
36 }
37 close(resChan)
38 close(stopChan)
39
40 if len(resultObs) != 0 {
41 t.Fail()
42 }
43 if len(hook.Entries) != 2 {
44 t.Fail()
45 }
46 }
47
48 func TestPacketbeatFormatEmpty(t *testing.T) {
49 hook := test.NewGlobal()
50
51 resultObs := make([]observation.InputObservation, 0)
52 resChan := make(chan observation.InputObservation)
53 go func() {
54 for o := range resChan {
55 resultObs = append(resultObs, o)
56 }
57 }()
58
59 stopChan := make(chan bool)
60 err := MakePacketbeatInputObservations([]byte(""), "foo", resChan, stopChan)
61 if err != nil {
62 t.Fatal(err)
63 }
64 close(resChan)
65 close(stopChan)
66
67 if len(resultObs) != 0 {
68 t.Fail()
69 }
70 if len(hook.Entries) != 1 {
71 t.Fail()
72 }
73 }
74
75 const exampleInPacketbeatInvalidTimestamp = `{
76 "type": "dns",
77 "dns": {
78 "answers": [{
79 "name": "foo.bar.",
80 "data": "1.2.3.4.",
81 "type": "A",
82 "class":"foo"
83 }]
84
85 },
86 "@timestamp": "2018-10-26T2"
87 }`
88
89 const exampleInPacketbeatInvalidType = `{
90 "type": "whatever",
91 "dns": {
92 "answers": [{
93 "name": "foo.bar.",
94 "data": "1.2.3.4.",
95 "type": "A",
96 "class":"foo"
97 }]
98
99 },
100 "@timestamp": "2018-10-26T21:03:20.222Z"
101 }`
102
103 const exampleInPacketbeat = `{
104 "type": "dns",
105 "dns": {
106 "answers": [{
107 "name": "foo.bar.",
108 "data": "1.2.3.4.",
109 "type": "A",
110 "class":"foo"
111 }]
112
113 },
114 "@timestamp": "2018-10-26T21:03:20.222Z"
115 }`
116
117 func TestPacketbeatFormat(t *testing.T) {
118 hook := test.NewGlobal()
119
120 resultObs := make([]observation.InputObservation, 0)
121 resChan := make(chan observation.InputObservation, 100)
122
123 stopChan := make(chan bool)
124 err := MakePacketbeatInputObservations([]byte(exampleInPacketbeat), "foo", resChan, stopChan)
125 if err != nil {
126 t.Fatal(err)
127 }
128 time.Sleep(500 * time.Millisecond)
129 close(resChan)
130 close(stopChan)
131
132 for o := range resChan {
133 resultObs = append(resultObs, o)
134 }
135
136 if len(resultObs) != 1 {
137 t.Fail()
138 }
139
140 if len(hook.Entries) != 1 {
141 t.Fail()
142 }
143 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package format
4
5 import (
6 "encoding/json"
7 "time"
8
9 "github.com/DCSO/balboa/observation"
10
11 log "github.com/sirupsen/logrus"
12 )
13
14 type suriDNSEntry struct {
15 EventType string `json:"event_type"`
16 Timestamp string `json:"timestamp"`
17 DNS struct {
18 Type string `json:"type"`
19 Version int `json:"version"`
20 Rrtype string `json:"rrtype"`
21 Rcode string `json:"rcode"`
22 Rrname string `json:"rrname"`
23 TTL int `json:"ttl"`
24 Rdata string `json:"rdata"`
25 Answers []struct {
26 Rrname string `json:"rrname"`
27 Rrtype string `json:"rrtype"`
28 TTL int `json:"ttl"`
29 Rdata string `json:"rdata"`
30 } `json:"answers"`
31 Grouped map[string]([]string) `json:"grouped"`
32 } `json:"dns"`
33 }
34
35 // MakeSuricataInputObservations is a MakeObservationFunc that accepts input
36 // in Suricata's EVE JSON format (DNS type version 1 and 2 are supported).
37 func MakeSuricataInputObservations(inputJSON []byte, sensorID string, out chan observation.InputObservation, stop chan bool) error {
38 var in suriDNSEntry
39 var i int
40 err := json.Unmarshal(inputJSON, &in)
41 if err != nil {
42 log.Warn(err)
43 return nil
44 }
45 if in.EventType != "dns" {
46 return nil
47 }
48 if in.DNS.Type != "answer" {
49 return nil
50 }
51 tst, err := time.Parse("2006-01-02T15:04:05.999999-0700", in.Timestamp)
52 if err != nil {
53 log.Warn(err)
54 return nil
55 }
56 if in.DNS.Version == 2 {
57 // v2 format
58 if len(in.DNS.Answers) > 0 {
59 // "detailed" format
60 for _, answer := range in.DNS.Answers {
61 o := observation.InputObservation{
62 Count: 1,
63 Rdata: answer.Rdata,
64 Rrname: answer.Rrname,
65 Rrtype: answer.Rrtype,
66 SensorID: sensorID,
67 TimestampEnd: tst,
68 TimestampStart: tst,
69 }
70 i++
71 out <- o
72 }
73 } else {
74 // "grouped" format
75 for rrtype, rdataArr := range in.DNS.Grouped {
76 for _, rdata := range rdataArr {
77 o := observation.InputObservation{
78 Count: 1,
79 Rdata: rdata,
80 Rrname: in.DNS.Rrname,
81 Rrtype: rrtype,
82 SensorID: sensorID,
83 TimestampEnd: tst,
84 TimestampStart: tst,
85 }
86 i++
87 out <- o
88 }
89 }
90 }
91 } else {
92 // v1 format
93 o := observation.InputObservation{
94 Count: 1,
95 Rdata: in.DNS.Rdata,
96 Rrname: in.DNS.Rrname,
97 Rrtype: in.DNS.Rrtype,
98 SensorID: sensorID,
99 TimestampEnd: tst,
100 TimestampStart: tst,
101 }
102 i++
103 out <- o
104 }
105 log.Infof("enqueued %d observations", i)
106 return nil
107 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package format
4
5 import (
6 "testing"
7 "time"
8
9 "github.com/DCSO/balboa/observation"
10 "github.com/sirupsen/logrus/hooks/test"
11 )
12
13 func TestSuricataFormatFail(t *testing.T) {
14 hook := test.NewGlobal()
15
16 resultObs := make([]observation.InputObservation, 0)
17 resChan := make(chan observation.InputObservation)
18 go func() {
19 for o := range resChan {
20 resultObs = append(resultObs, o)
21 }
22 }()
23
24 stopChan := make(chan bool)
25 err := MakeSuricataInputObservations([]byte(`babanana`), "foo", resChan, stopChan)
26 if err != nil {
27 t.Fatal(err)
28 }
29 err = MakeSuricataInputObservations([]byte(exampleInSuricataInvalidTimestamp), "foo", resChan, stopChan)
30 if err != nil {
31 t.Fatal(err)
32 }
33 err = MakeSuricataInputObservations([]byte(exampleInSuricataInvalidType), "foo", resChan, stopChan)
34 if err != nil {
35 t.Fatal(err)
36 }
37 err = MakeSuricataInputObservations([]byte(exampleInSuricataInvalidType2), "foo", resChan, stopChan)
38 if err != nil {
39 t.Fatal(err)
40 }
41 close(resChan)
42 close(stopChan)
43
44 if len(resultObs) != 0 {
45 t.Fail()
46 }
47 if len(hook.Entries) != 2 {
48 t.Fail()
49 }
50 }
51
52 func TestSuricataFormatEmpty(t *testing.T) {
53 hook := test.NewGlobal()
54
55 resultObs := make([]observation.InputObservation, 0)
56 resChan := make(chan observation.InputObservation)
57 go func() {
58 for o := range resChan {
59 resultObs = append(resultObs, o)
60 }
61 }()
62
63 stopChan := make(chan bool)
64 err := MakeSuricataInputObservations([]byte(""), "foo", resChan, stopChan)
65 if err != nil {
66 t.Fatal(err)
67 }
68 close(resChan)
69 close(stopChan)
70
71 if len(resultObs) != 0 {
72 t.Fail()
73 }
74 if len(hook.Entries) != 1 {
75 t.Fail()
76 }
77 }
78
79 const exampleInSuricataInvalidTimestamp = `{
80 "timestamp": "2009-11-24T21:",
81 "event_type": "dns",
82 "src_ip": "192.168.2.7",
83 "src_port": 53,
84 "dest_ip": "x.x.250.50",
85 "dest_port": 23242,
86 "proto": "UDP",
87 "dns": {
88 "type": "answer",
89 "id":16000,
90 "flags":"8180",
91 "qr":true,
92 "rd":true,
93 "ra":true,
94 "rcode":"NOERROR",
95 "rrname": "twitter.com",
96 "rrtype":"A",
97 "ttl":8,
98 "rdata": "199.16.156.6"
99 }
100 }`
101
102 const exampleInSuricataInvalidType = `{
103 "timestamp": "2009-11-24T21:27:09.534255-0100",
104 "event_type": "foo",
105 "src_ip": "192.168.2.7",
106 "src_port": 53,
107 "dest_ip": "x.x.250.50",
108 "dest_port": 23242,
109 "proto": "UDP",
110 "dns": {
111 "type": "answer",
112 "id":16000,
113 "flags":"8180",
114 "qr":true,
115 "rd":true,
116 "ra":true,
117 "rcode":"NOERROR",
118 "rrname": "twitter.com",
119 "rrtype":"A",
120 "ttl":8,
121 "rdata": "199.16.156.6"
122 }
123 }`
124
125 const exampleInSuricataInvalidType2 = `{
126 "timestamp": "2009-11-24T21:27:09.534255-0100",
127 "event_type": "dns",
128 "src_ip": "192.168.2.7",
129 "src_port": 53,
130 "dest_ip": "x.x.250.50",
131 "dest_port": 23242,
132 "proto": "UDP",
133 "dns": {
134 "type": "foo",
135 "id":16000,
136 "flags":"8180",
137 "qr":true,
138 "rd":true,
139 "ra":true,
140 "rcode":"NOERROR",
141 "rrname": "twitter.com",
142 "rrtype":"A",
143 "ttl":8,
144 "rdata": "199.16.156.6"
145 }
146 }`
147
148 const exampleInSuricataV2 = `{
149 "timestamp": "2009-11-24T21:27:09.534255-0100",
150 "event_type": "dns",
151 "src_ip": "192.168.2.7",
152 "src_port": 53,
153 "dest_ip": "x.x.250.50",
154 "dest_port": 23242,
155 "proto": "UDP",
156 "dns": {
157 "version": 2,
158 "type": "answer",
159 "id": 45444,
160 "flags": "8180",
161 "qr": true,
162 "rd": true,
163 "ra": true,
164 "rcode": "NOERROR",
165 "answers": [
166 {
167 "rrname": "www.suricata-ids.org",
168 "rrtype": "CNAME",
169 "ttl": 3324,
170 "rdata": "suricata-ids.org"
171 },
172 {
173 "rrname": "suricata-ids.org",
174 "rrtype": "A",
175 "ttl": 10,
176 "rdata": "192.0.78.24"
177 },
178 {
179 "rrname": "suricata-ids.org",
180 "rrtype": "A",
181 "ttl": 10,
182 "rdata": "192.0.78.25"
183 }
184 ]
185 }
186 }`
187
188 const exampleInSuricataV2Grouped = `{
189 "timestamp": "2009-11-24T21:27:09.534255-0100",
190 "event_type": "dns",
191 "src_ip": "192.168.2.7",
192 "src_port": 53,
193 "dest_ip": "x.x.250.50",
194 "dest_port": 23242,
195 "proto": "UDP",
196 "dns": {
197 "version": 2,
198 "type": "answer",
199 "id": 18523,
200 "flags": "8180",
201 "qr": true,
202 "rd": true,
203 "ra": true,
204 "rcode": "NOERROR",
205 "grouped": {
206 "A": [
207 "192.0.78.24",
208 "192.0.78.25"
209 ],
210 "CNAME": [
211 "suricata-ids.org"
212 ]
213 }
214 }
215 }`
216
217 const exampleInSuricataV1 = `{
218 "timestamp": "2009-11-24T21:27:09.534255-0100",
219 "event_type": "dns",
220 "src_ip": "192.168.2.7",
221 "src_port": 53,
222 "dest_ip": "x.x.250.50",
223 "dest_port": 23242,
224 "proto": "UDP",
225 "dns": {
226 "type": "answer",
227 "id":16000,
228 "flags":"8180",
229 "qr":true,
230 "rd":true,
231 "ra":true,
232 "rcode":"NOERROR",
233 "rrname": "twitter.com",
234 "rrtype":"A",
235 "ttl":8,
236 "rdata": "199.16.156.6"
237 }
238 }`
239
240 func TestSuricataFormat(t *testing.T) {
241 hook := test.NewGlobal()
242
243 resultObs := make([]observation.InputObservation, 0)
244 resChan := make(chan observation.InputObservation, 100)
245
246 stopChan := make(chan bool)
247 err := MakeSuricataInputObservations([]byte(exampleInSuricataV1), "foo", resChan, stopChan)
248 if err != nil {
249 t.Fatal(err)
250 }
251 err = MakeSuricataInputObservations([]byte(exampleInSuricataV2), "foo", resChan, stopChan)
252 if err != nil {
253 t.Fatal(err)
254 }
255 err = MakeSuricataInputObservations([]byte(exampleInSuricataV2Grouped), "foo", resChan, stopChan)
256 if err != nil {
257 t.Fatal(err)
258 }
259 time.Sleep(500 * time.Millisecond)
260 close(resChan)
261 close(stopChan)
262
263 for o := range resChan {
264 resultObs = append(resultObs, o)
265 }
266
267 if len(resultObs) != 7 {
268 t.Fail()
269 }
270 if len(hook.Entries) != 3 {
271 t.Fail()
272 }
273 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package observation
4
5 import (
6 "time"
7 )
8
9 // InputObservation is a minimal, small observation structure to be used as
10 // the minimal common input type for all consumers.
11 type InputObservation struct {
12 Count int
13 Rcode string
14 Rdata string
15 Rrtype string
16 Rrname string
17 SensorID string
18 TimestampEnd time.Time
19 TimestampStart time.Time
20 }
21
22 // InChan is the global input channel delivering InputObservations from
23 // feeders to consumers.
24 var InChan chan InputObservation
25
26 func init() {
27 InChan = make(chan InputObservation, 50000)
28 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package observation
4
5 import (
6 uuid "github.com/satori/go.uuid"
7 )
8
9 // Observation represents a DNS answer, potentially repeated, observed on a
10 // given sensor stating a specific RR set.
11 type Observation struct {
12 ID uuid.UUID `json:"-"`
13 Count int `json:"count"`
14 FirstSeen int `json:"time_first"`
15 LastSeen int `json:"time_last"`
16 RRType string `json:"rrtype"`
17 RRName string `json:"rrname"`
18 RData string `json:"rdata"`
19 SensorID string `json:"sensor_id"`
20 }
0 // balboa
1 // Copyright (c) 2018, DCSO GmbH
2
3 package query
4
5 import (
6 "context"
7 "fmt"
8 "net/http"
9 "runtime"
10 "time"
11
12 "github.com/DCSO/balboa/db"
13 "github.com/DCSO/balboa/observation"
14
15 graphql "github.com/graph-gophers/graphql-go"
16 "github.com/graph-gophers/graphql-go/errors"
17 "github.com/graph-gophers/graphql-go/relay"
18 uuid "github.com/satori/go.uuid"
19 log "github.com/sirupsen/logrus"
20 )
21
22 const (
23 txtSchema = `# A DNS resource record type.
24 enum RRType {
25 A
26 A6
27 AAAA
28 AFSDB
29 ALIAS
30 APL
31 AXFR
32 CAA
33 CDNSKEY
34 CDS
35 CERT
36 CNAME
37 DHCID
38 DLV
39 DNAME
40 DNSKEY
41 DS
42 HINFO
43 HIP
44 IPSECKEY
45 IXFR
46 KEY
47 KX
48 LOC
49 MX
50 NAPTR
51 NS
52 NSEC
53 NSEC3
54 NSEC3PARAM
55 OPENPGPKEY
56 OPT
57 PTR
58 RRSIG
59 RP
60 SIG
61 SOA
62 SPF
63 SRV
64 SSHFP
65 TA
66 TKEY
67 TLSA
68 TSIG
69 TXT
70 URI
71 }
72
73 # A single observation, unique for the combination of sensor, rrname,
74 # rdata and rrtype. Corresponds, roughly, to a pDNS COF item, but with
75 # additional Aliases (linked via IP in A/AAAA records).
76 type Entry {
77 # The number of observed occurrences of this observation.
78 count: Int!
79
80 # The RRName seen in this observation.
81 rrname: String!
82
83 # The RRType seen in this observation.
84 rrtype: RRType
85
86 # The RData seen in this observation.
87 rdata: String!
88
89 # Time this observation was first seen, as Unix timestamp.
90 time_first: Int!
91
92 # Time this observation was first seen, as RFC 3339 formatted time.
93 time_first_rfc3339: String!
94
95 # Time this observation was last seen, as Unix timestamp.
96 time_last: Int!
97
98 # Time this observation was last seen, as RFC 3339 formatted time.
99 time_last_rfc3339: String!
100
101 # Some identifier describing the source of this observation.
102 sensor_id: String
103
104 # Entries referencing the same IP (for A/AAAA) observed on the same
105 # sensor.
106 aliases: [LeafEntry]
107 }
108
109 # A single observation, unique for the combination of sensor, rrname,
110 # rdata and rrtype. Corresponds, roughly, to a pDNS COF item.
111 type LeafEntry {
112 # The number of observed occurrences of this observation.
113 count: Int!
114
115 # The RRName seen in this observation.
116 rrname: String!
117
118 # The RRType seen in this observation.
119 rrtype: RRType
120
121 # The RData seen in this observation.
122 rdata: String!
123
124 # Time this observation was first seen, as Unix timestamp.
125 time_first: Int!
126
127 # Time this observation was first seen, as RFC 3339 formatted time.
128 time_first_rfc3339: String!
129
130 # Time this observation was last seen, as Unix timestamp.
131 time_last: Int!
132
133 # Time this observation was last seen, as RFC 3339 formatted time.
134 time_last_rfc3339: String!
135
136 # Some identifier describing the source of this observation.
137 sensor_id: String
138 }
139
140 input EntryInput {
141 # The number of observed occurrences of this observation.
142 count: Int!
143
144 # The RRName seen in this observation.
145 rrname: String!
146
147 # The RRType seen in this observation.
148 rrtype: RRType!
149
150 # The RData seen in this observation.
151 rdata: String!
152
153 # Time this observation was first seen, as Unix timestamp.
154 time_first: Int!
155
156 # Time this observation was last seen, as Unix timestamp.
157 time_last: Int!
158
159 # Some identifier describing the source of this observation.
160 sensor_id: String!
161 }
162
163 # Some runtime values describing the current state of the database.
164 type Stats {
165 # Total number of keys in the database.
166 total_count: Int!
167
168 # Number of concurrent goroutines in the server instance.
169 num_goroutines: Int!
170 }
171
172 type Query {
173 # Returns a set of observations satisfying the given query parameters.
174 # Providing rdata, rrname, rrtype and/or sensor_id will restrict the
175 # results to the set of observations that match all of the given
176 # constraints.
177 entries(rdata: String, rrname: String, rrtype: RRType, sensor_id: String): [Entry]
178
179 # Returns some runtime values describing the current state of the database.
180 stats(): Stats
181 }
182
183 type Mutation {
184 announceObservation(observation: EntryInput!): Entry!
185 }
186
187 schema {
188 query: Query
189 mutation: Mutation
190 }`
191 )
192
193 // GraphQLFrontend represents a concurrent component that provides a GraphQL
194 // query interface for the database.
195 type GraphQLFrontend struct {
196 Server *http.Server
197 IsRunning bool
198 }
199
200 // Resolver is just used to bundle top level methods.
201 type Resolver struct{}
202
203 type entryInputArgs struct {
204 Count int
205 FirstSeen int
206 LastSeen int
207 RRType string
208 RRName string
209 RData string
210 SensorID string
211 }
212
213 // Entries returns a collection of Entry resolvers, given parameters such as
214 // Rdata, RRname, RRtype and sensor ID.
215 func (r *Resolver) Entries(args struct {
216 Rdata *string
217 Rrname *string
218 Rrtype *string
219 SensorID *string
220 }) (*[]*EntryResolver, error) {
221 startTime := time.Now()
222 defer func() {
223 var rdata, rrname, rrtype, sensorID string
224 if args.Rdata != nil {
225 rdata = *args.Rdata
226 } else {
227 rdata = ("nil")
228 }
229 if args.Rrname != nil {
230 rrname = *args.Rrname
231 } else {
232 rrname = ("nil")
233 }
234 if args.Rrtype != nil {
235 rrtype = *args.Rrtype
236 } else {
237 rrtype = ("nil")
238 }
239 if args.SensorID != nil {
240 sensorID = *args.SensorID
241 } else {
242 sensorID = ("nil")
243 }
244 log.Debugf("finished query for (%s/%s/%s/%s) in %v", rdata, rrname, rrtype, sensorID, time.Since(startTime))
245 }()
246
247 l := make([]*EntryResolver, 0)
248 if args.Rdata == nil && args.Rrname == nil {
249 return nil, &errors.QueryError{
250 Message: "at least one of the 'rdata' or 'rrname' parameters is required",
251 }
252 }
253 results, err := db.ObservationDB.Search(args.Rdata, args.Rrname, args.Rrtype, args.SensorID)
254 if err != nil {
255 return nil, err
256 }
257 for _, r := range results {
258 er := EntryResolver{
259 entry: r,
260 }
261 l = append(l, &er)
262 }
263 return &l, nil
264 }
265
266 // AnnounceObservation is a mutation that adds a single new observation
267 // to the database.
268 func (r *Resolver) AnnounceObservation(args struct {
269 Observation struct {
270 Count int32
271 TimeFirst int32
272 TimeLast int32
273 RRType string
274 RRName string
275 RData string
276 SensorID string
277 }
278 }) *EntryResolver {
279 inObs := observation.InputObservation{
280 Count: int(args.Observation.Count),
281 TimestampStart: time.Unix(int64(args.Observation.TimeFirst), 0),
282 TimestampEnd: time.Unix(int64(args.Observation.TimeLast), 0),
283 Rrname: args.Observation.RRName,
284 Rrtype: args.Observation.RRType,
285 Rdata: args.Observation.RData,
286 SensorID: args.Observation.SensorID,
287 }
288 resObs := db.ObservationDB.AddObservation(inObs)
289 return &EntryResolver{
290 entry: resObs,
291 }
292 }
293
294 // Stats returns a Stats resolver.
295 func (r *Resolver) Stats() (*StatsResolver, error) {
296 return &StatsResolver{}, nil
297 }
298
299 // StatsResolver is a resolver for the Stats type.
300 type StatsResolver struct {
301 totalCount int32
302 }
303
304 // TotalCount returns the total number of keys in the database.
305 func (r *StatsResolver) TotalCount() int32 {
306 val, err := db.ObservationDB.TotalCount()
307 if err != nil {
308 log.Error(err)
309 }
310 return int32(val)
311 }
312
313 // NumGoroutines returns the number of currently running goroutines
314 // in balboa.
315 func (r *StatsResolver) NumGoroutines() int32 {
316 return int32(runtime.NumGoroutine())
317 }
318
319 // EntryResolver is a resolver for the Entry type.
320 type EntryResolver struct {
321 entry observation.Observation
322 }
323
324 // ID returns the ID field of the corresponding entry.
325 func (r *EntryResolver) ID() graphql.ID {
326 id, _ := uuid.NewV4()
327 return graphql.ID(id.String())
328 }
329
330 // RRName returns the RRName field of the corresponding entry.
331 func (r *EntryResolver) RRName() string {
332 return r.entry.RRName
333 }
334
335 // Rdata returns the Rdata field of the corresponding entry.
336 func (r *EntryResolver) Rdata() string {
337 return r.entry.RData
338 }
339
340 // RRType returns the RRType field of the corresponding entry.
341 func (r *EntryResolver) RRType() *string {
342 return &r.entry.RRType
343 }
344
345 // Count returns the Count field of the corresponding entry.
346 func (r *EntryResolver) Count() int32 {
347 return int32(r.entry.Count)
348 }
349
350 // TimeFirst returns the first seen timestamp of the corresponding entry.
351 func (r *EntryResolver) TimeFirst() int32 {
352 return int32(r.entry.FirstSeen)
353 }
354
355 // TimeFirstRFC3339 returns first seen time, as RFC 3339 string, of the corresponding entry.
356 func (r *EntryResolver) TimeFirstRFC3339() string {
357 return time.Unix(int64(r.entry.FirstSeen), 0).Format(time.RFC3339)
358 }
359
360 // TimeLast returns the last seen timestamp of the corresponding entry.
361 func (r *EntryResolver) TimeLast() int32 {
362 return int32(r.entry.LastSeen)
363 }
364
365 // TimeLastRFC3339 returns last seen time, as RFC 3339 string, of the corresponding entry.
366 func (r *EntryResolver) TimeLastRFC3339() string {
367 return time.Unix(int64(r.entry.LastSeen), 0).Format(time.RFC3339)
368 }
369
370 // SensorID returns the sensor ID field of the corresponding entry.
371 func (r *EntryResolver) SensorID() *string {
372 return &r.entry.SensorID
373 }
374
375 // Aliases returns resolvers for Entries with the same IPs in Rdata (for
376 // A/AAAA type entries).
377 func (r *EntryResolver) Aliases() *[]*EntryResolver {
378 l := make([]*EntryResolver, 0)
379 if !(r.entry.RRType == "A" || r.entry.RRType == "AAAA") {
380 return nil
381 }
382 results, err := db.ObservationDB.Search(&r.entry.RData, nil, nil, &r.entry.SensorID)
383 if err != nil {
384 return nil
385 }
386 for _, rs := range results {
387 if rs.RRName != r.entry.RRName {
388 er := EntryResolver{
389 entry: rs,
390 }
391 l = append(l, &er)
392 }
393 }
394 return &l
395 }
396
397 // Run starts this instance of a GraphQLFrontend in the background, accepting
398 // new requests on the configured port.
399 func (g *GraphQLFrontend) Run(port int) {
400 schema := graphql.MustParseSchema(txtSchema, &Resolver{})
401 g.Server = &http.Server{
402 Addr: fmt.Sprintf(":%v", port),
403 Handler: &relay.Handler{Schema: schema},
404 ReadTimeout: 5 * time.Second,
405 WriteTimeout: 10 * time.Second,
406 }
407 log.Infof("serving GraphQL on port %v", port)
408 go func() {
409 err := g.Server.ListenAndServe()
410 if err != nil {
411 log.Info(err)
412 }
413 g.IsRunning = true
414 }()
415 }
416
417 // Stop causes this instance of a GraphQLFrontend to cease accepting requests.
418 func (g *GraphQLFrontend) Stop() {
419 if g.IsRunning {
420 g.Server.Shutdown(context.TODO())
421 }
422 }
0 #!/usr/bin/env python
1 import datetime
2 import random
3 import string
4 import json
5 import socket
6 import struct
7 import time
8 import sys
9
10 def _timezone(utc_offset):
11 '''
12 Return a string representing the timezone offset.
13
14 >>> _timezone(0)
15 '+00:00'
16 >>> _timezone(3600)
17 '+01:00'
18 >>> _timezone(-28800)
19 '-08:00'
20 >>> _timezone(-8 * 60 * 60)
21 '-08:00'
22 >>> _timezone(-30 * 60)
23 '-00:30'
24 '''
25 # Python's division uses floor(), not round() like in other languages:
26 # -1 / 2 == -1 and not -1 / 2 == 0
27 # That's why we use abs(utc_offset).
28 hours = abs(utc_offset) // 3600
29 minutes = abs(utc_offset) % 3600 // 60
30 sign = (utc_offset < 0 and '-') or '+'
31 return '%c%02d:%02d' % (sign, hours, minutes)
32
33 def _timedelta_to_seconds(td):
34 '''
35 >>> _timedelta_to_seconds(datetime.timedelta(hours=3))
36 10800
37 >>> _timedelta_to_seconds(datetime.timedelta(hours=3, minutes=15))
38 11700
39 >>> _timedelta_to_seconds(datetime.timedelta(hours=-8))
40 -28800
41 '''
42 return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
43
44 def _utc_offset(date, use_system_timezone):
45 '''
46 Return the UTC offset of `date`. If `date` does not have any `tzinfo`, use
47 the timezone informations stored locally on the system.
48
49 >>> if time.localtime().tm_isdst:
50 ... system_timezone = -time.altzone
51 ... else:
52 ... system_timezone = -time.timezone
53 >>> _utc_offset(datetime.datetime.now(), True) == system_timezone
54 True
55 >>> _utc_offset(datetime.datetime.now(), False)
56 0
57 '''
58 if isinstance(date, datetime.datetime) and date.tzinfo is not None:
59 return _timedelta_to_seconds(date.utcoffset())
60 elif use_system_timezone:
61 if date.year < 1970:
62 # We use 1972 because 1970 doesn't have a leap day (feb 29)
63 t = time.mktime(date.replace(year=1972).timetuple())
64 else:
65 t = time.mktime(date.timetuple())
66 if time.localtime(t).tm_isdst: # pragma: no cover
67 return -time.altzone
68 else:
69 return -time.timezone
70 else:
71 return 0
72
73 def _string(d, timezone):
74 return ('%04d-%02d-%02dT%02d:%02d:%02d%s' %
75 (d.year, d.month, d.day, d.hour, d.minute, d.second, timezone))
76
77 def format(date, utc=False, use_system_timezone=True):
78 '''
79 Return a string formatted according to the :RFC:`3339`. If called with
80 `utc=True`, it normalizes `date` to the UTC date. If `date` does not have
81 any timezone information, uses the local timezone::
82
83 >>> d = datetime.datetime(2008, 4, 2, 20)
84 >>> rfc3339(d, utc=True, use_system_timezone=False)
85 '2008-04-02T20:00:00Z'
86 >>> rfc3339(d) # doctest: +ELLIPSIS
87 '2008-04-02T20:00:00...'
88
89 If called with `use_system_timezone=False` don't use the local timezone if
90 `date` does not have timezone informations and consider the offset to UTC
91 to be zero::
92
93 >>> rfc3339(d, use_system_timezone=False)
94 '2008-04-02T20:00:00+00:00'
95
96 `date` must be a `datetime.datetime`, `datetime.date` or a timestamp as
97 returned by `time.time()`::
98
99 >>> rfc3339(0, utc=True, use_system_timezone=False)
100 '1970-01-01T00:00:00Z'
101 >>> rfc3339(datetime.date(2008, 9, 6), utc=True,
102 ... use_system_timezone=False)
103 '2008-09-06T00:00:00Z'
104 >>> rfc3339(datetime.date(2008, 9, 6),
105 ... use_system_timezone=False)
106 '2008-09-06T00:00:00+00:00'
107 >>> rfc3339('foo bar')
108 Traceback (most recent call last):
109 ...
110 TypeError: Expected timestamp or date object. Got <type 'str'>.
111
112 For dates before January 1st 1970, the timezones will be the ones used in
113 1970. It might not be accurate, but on most sytem there is no timezone
114 information before 1970.
115 '''
116 # Try to convert timestamp to datetime
117 try:
118 if use_system_timezone:
119 date = datetime.datetime.fromtimestamp(date)
120 else:
121 date = datetime.datetime.utcfromtimestamp(date)
122 except TypeError:
123 pass
124
125 if not isinstance(date, datetime.date):
126 raise TypeError('Expected timestamp or date object. Got %r.' %
127 type(date))
128
129 if not isinstance(date, datetime.datetime):
130 date = datetime.datetime(*date.timetuple()[:3])
131 utc_offset = _utc_offset(date, use_system_timezone)
132 if utc:
133 # local time -> utc
134 return _string(date - datetime.timedelta(seconds=utc_offset), 'Z')
135 else:
136 return _string(date, _timezone(utc_offset))
137
138 def string2numeric_hash(text):
139 import hashlib
140 return int(hashlib.md5(text).hexdigest()[:8], 16)
141
142 entry = {}
143 entry["dns"] = {}
144 entry["timestamp_start"] = format(datetime.datetime.now())
145 entry["timestamp_end"] = format(datetime.datetime.now() + + datetime.timedelta(minutes=1))
146 for i in range(random.randint(1, 20000)):
147 rrname = ''.join(random.choice(string.ascii_lowercase) for _ in range(5))
148 rdata = socket.inet_ntoa(struct.pack('>I', string2numeric_hash(rrname)))
149 entry["dns"][(rrname + ".com")] = {"rdata": [{"rrtype": random.choice(["A", "NS", "MX"]), "rdata": rdata, "answering_host": "8.8.8.8", "count": 1, "rcode": "NOERROR"}]}
150
151 print(json.dumps(entry))