├── .dockerignore
├── .gitignore
├── Dockerfile.example
├── INSTALL.md
├── LICENSE
├── Makefile.example
├── README.md
├── VERSION
├── account.go
├── admin.go
├── api.go
├── assets
├── css
│ ├── admin.css
│ ├── c3.min.css
│ ├── common.css
│ ├── docs.css
│ ├── fontawesome.min.css
│ ├── github-markdown.css
│ ├── index.css
│ ├── profile.css
│ ├── semantic-2.8.6.min.css
│ ├── signin.css
│ └── themes
│ │ ├── basic
│ │ └── assets
│ │ │ └── fonts
│ │ │ ├── icons.eot
│ │ │ ├── icons.svg
│ │ │ ├── icons.ttf
│ │ │ └── icons.woff
│ │ ├── default
│ │ └── assets
│ │ │ ├── fonts
│ │ │ ├── brand-icons.eot
│ │ │ ├── brand-icons.svg
│ │ │ ├── brand-icons.ttf
│ │ │ ├── brand-icons.woff
│ │ │ ├── brand-icons.woff2
│ │ │ ├── icons.eot
│ │ │ ├── icons.svg
│ │ │ ├── icons.ttf
│ │ │ ├── icons.woff
│ │ │ ├── icons.woff2
│ │ │ ├── outline-icons.eot
│ │ │ ├── outline-icons.svg
│ │ │ ├── outline-icons.ttf
│ │ │ ├── outline-icons.woff
│ │ │ └── outline-icons.woff2
│ │ │ └── images
│ │ │ └── flags.png
│ │ ├── github
│ │ └── assets
│ │ │ └── fonts
│ │ │ ├── octicons-local.ttf
│ │ │ ├── octicons.svg
│ │ │ ├── octicons.ttf
│ │ │ └── octicons.woff
│ │ └── material
│ │ └── assets
│ │ └── fonts
│ │ ├── icons.eot
│ │ ├── icons.svg
│ │ ├── icons.ttf
│ │ ├── icons.woff
│ │ └── icons.woff2
├── fonts
│ ├── Roboto-Bold.ttf
│ ├── Roboto-Medium.ttf
│ ├── Roboto-Regular.ttf
│ ├── RobotoMono-Medium.ttf
│ ├── RobotoMono-Regular.ttf
│ ├── fontawesome-webfont.ttf
│ ├── fontawesome-webfont.woff
│ └── fontawesome-webfont.woff2
├── img
│ ├── architecture.png
│ ├── architecture.svg
│ ├── autocomplete.png
│ ├── cef.png
│ ├── cluster.png
│ ├── datasources.png
│ ├── filters.png
│ ├── logo.svg
│ ├── nodes-images.png
│ ├── red-filter.png
│ ├── results-extended.png
│ ├── results-styled.png
│ ├── results.png
│ ├── screen.png
│ ├── stats.png
│ ├── ui-demo.gif
│ ├── ui-elements.png
│ ├── ui-elements.xcf
│ └── users.png
├── js
│ ├── account.js
│ ├── admin-actions.js
│ ├── admin.js
│ ├── c3.min.js
│ ├── calendar.js
│ ├── charts.js
│ ├── d3.v5.min.js
│ ├── dashboards.js
│ ├── docs.js
│ ├── features.js
│ ├── filters.js
│ ├── graph.js
│ ├── index.js
│ ├── jquery-3.4.1.min.js
│ ├── markdown-it.min-11.0.0.js
│ ├── markdownItAnchor.umd.js
│ ├── markdownItAnchor.umd.js.map
│ ├── markdownItTocDoneRight.umd.js
│ ├── markdownItTocDoneRight.umd.js.map
│ ├── modal.js
│ ├── notifications.js
│ ├── options.js
│ ├── profile.js
│ ├── search.js
│ ├── semantic-2.8.6.min.js
│ ├── settings.js
│ ├── sql-autocomplete.js
│ ├── tags.js
│ ├── user-actions.js
│ ├── users.js
│ ├── vis-network.min-9.0.4.js
│ ├── vis-network.min.js.map
│ └── websocket.js
└── tmpl
│ ├── admin.html
│ ├── credits.html
│ ├── docs.html
│ ├── index.html
│ ├── modal.html
│ ├── profile.html
│ ├── signin.html
│ └── topbar.html
├── certs
├── graphoscope.crt
└── graphoscope.key
├── config.go
├── dashboards.go
├── database.go
├── definitions
├── outputs
│ └── output.yaml.example
├── processors
│ └── processor.yaml.example
└── sources
│ ├── demo.yaml.example
│ └── source.yaml.example
├── docs.go
├── docs
├── admin.md
├── search.md
└── ui.md
├── features.go
├── files
├── demo.csv
├── features.yaml
├── formats.yaml.example
├── graphoscope.service
└── groups.json.example
├── filters.go
├── go.mod
├── go.sum
├── graphoscope.yaml.example
├── gui.go
├── handlers.go
├── loaders.go
├── logger.go
├── main.go
├── mongostore.go
├── notifications.go
├── parse.go
├── pdk
├── common.go
├── plugin.go
├── processor.go
├── source.go
└── stats.go
├── plugins
└── src
│ ├── abuseipdb
│ ├── README.md
│ ├── abuseipdb.go
│ ├── abuseipdb_test.go
│ ├── convert.go
│ ├── plugin.go
│ └── select.go
│ ├── circl_passive_ssl
│ ├── README.md
│ ├── convert.go
│ ├── passive_ssl.go
│ ├── passive_ssl_test.go
│ ├── plugin.go
│ └── select.go
│ ├── elasticsearch.v7
│ ├── README.md
│ ├── convert.go
│ ├── elasticsearch.go
│ ├── elasticsearch_test.go
│ ├── plugin.go
│ └── select.go
│ ├── elasticsearch.v8
│ ├── README.md
│ ├── convert.go
│ ├── elasticsearch.go
│ ├── elasticsearch_test.go
│ ├── plugin.go
│ └── select.go
│ ├── file
│ └── csv
│ │ ├── README.md
│ │ ├── convert.go
│ │ ├── csv.go
│ │ ├── csv_test.go
│ │ └── plugin.go
│ ├── hashlookup
│ ├── README.md
│ ├── convert.go
│ ├── hashlookup.go
│ ├── hashlookup_test.go
│ ├── plugin.go
│ └── select.go
│ ├── http
│ ├── README.md
│ ├── convert.go
│ ├── http.go
│ ├── http_test.go
│ ├── plugin.go
│ └── select.go
│ ├── ipinfo
│ ├── README.md
│ ├── convert.go
│ ├── ipinfo.go
│ ├── ipinfo_test.go
│ ├── plugin.go
│ └── select.go
│ ├── misp
│ ├── README.md
│ ├── convert.go
│ ├── misp.go
│ ├── misp_test.go
│ ├── package.go
│ ├── plugin.go
│ └── select.go
│ ├── modify
│ ├── README.md
│ ├── modify.go
│ ├── modify_test.go
│ └── plugin.go
│ ├── mongodb
│ ├── README.md
│ ├── convert.go
│ ├── mongodb.go
│ ├── mongodb_test.go
│ ├── plugin.go
│ └── select.go
│ ├── mysql
│ ├── README.md
│ ├── convert.go
│ ├── mysql.go
│ ├── mysql_test.go
│ └── plugin.go
│ ├── pastelyzer
│ ├── README.md
│ ├── convert.go
│ ├── pastelyzer.go
│ ├── pastelyzer_test.go
│ ├── plugin.go
│ └── select.go
│ ├── phishtank
│ ├── README.md
│ ├── convert.go
│ ├── phishtank.go
│ ├── phishtank_test.go
│ ├── plugin.go
│ └── select.go
│ ├── postgresql
│ ├── README.md
│ ├── convert.go
│ ├── plugin.go
│ ├── postgresql.go
│ └── postgresql_test.go
│ ├── redis
│ ├── README.md
│ ├── convert.go
│ ├── plugin.go
│ ├── redis.go
│ ├── redis_test.go
│ └── select.go
│ ├── rest
│ ├── README.md
│ ├── convert.go
│ ├── plugin.go
│ ├── rest.go
│ ├── rest_test.go
│ └── select.go
│ ├── shodan
│ ├── README.md
│ ├── convert.go
│ ├── plugin.go
│ ├── select.go
│ ├── shodan.go
│ └── shodan_test.go
│ ├── sqlite
│ ├── README.md
│ ├── convert.go
│ ├── plugin.go
│ ├── sqlite.go
│ └── sqlite_test.go
│ ├── taxonomy
│ ├── README.md
│ ├── plugin.go
│ ├── taxonomy.go
│ └── taxonomy_test.go
│ └── template
│ ├── README.md
│ ├── convert.go
│ ├── plugin.go
│ ├── template.go
│ └── template_test.go
├── profile.go
├── replace.go
├── response.go
├── session.go
├── sources.go
├── upload.go
└── websocket.go
/.dockerignore:
--------------------------------------------------------------------------------
1 | certs
2 | definitions
3 | plugins/*.so*
4 | files
5 | graphoscope*
6 | upload
7 | ideas
8 | build
9 | binary
10 | Makefile*
11 | Dockerfile*
12 | *.yaml
13 | *.log
14 | *.log.gz
15 | *.pprof
16 | profile*.svg
17 | *.sublime-project
18 | *.sublime-workspace
19 | .buildconfig
20 | tags.*
21 | assets
22 | docs
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | certs/*
2 | !certs/graphoscope.*
3 | definitions/*.yaml
4 | !definitions/sources/*.example
5 | !definitions/processors/*.example
6 | !definitions/outputs/*.example
7 | plugins/sources/*.so*
8 | plugins/processors/*.so*
9 | plugins/outputs/*.so*
10 | *.yaml
11 | files/groups.json
12 | files/formats.yaml
13 | !files/features.yaml
14 | graphoscope
15 | upload
16 | ideas
17 | build
18 | binary
19 | Makefile
20 | Dockerfile
21 | *.log
22 | *.log.gz
23 | *.pprof
24 | profile*.svg
25 | *.sublime-project
26 | *.sublime-workspace
27 | .buildconfig
28 | tags.*
29 | !assets/js/tags.js
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 2.6.0
--------------------------------------------------------------------------------
/assets/css/admin.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Top
3 | */
4 | .ui.grid {
5 | width: 33%;
6 | min-width: 700px;
7 | margin: 20px auto 0 auto;
8 | }
9 |
10 | /*
11 | * Content style
12 | */
13 | .ui.two.column.grid .row .column span {
14 | font-weight: bold;
15 | margin-top: 9px;
16 | position: absolute;
17 | }
18 |
19 | .ui.input>input {
20 | line-height: 1.3em;
21 | }
22 |
23 | .ui.checkbox {
24 | float: right;
25 | margin-top: 8px;
26 | }
27 |
28 | .ui.grid>.row.username {
29 | padding-bottom: 2rem;
30 | }
31 | .ui.grid>.row.short {
32 | padding-bottom: 0;
33 | }
34 |
35 | .ui.grid.settings>.row>.column:first-child {
36 | width: 20%;
37 | }
38 | .ui.grid.settings>.row>.column:last-child {
39 | width: 80%;
40 | }
41 |
42 | .ui.grid .info.row {
43 | padding-bottom: 0;
44 | }
45 |
46 | .ui.grid .ui.message {
47 | width: 100%;
48 | box-shadow: none;
49 | -webkit-box-shadow: none;
50 | }
51 |
52 | .ui[class*="two column"].grid>.row>.column .ui.input {
53 | width: 100%;
54 | }
55 |
56 | /*
57 | * Users mng
58 | */
59 | .ui.grid.users > .row {
60 | align-items: center;
61 | }
62 |
63 | .ui.grid.users > .row.th {
64 | font-size: 14px;
65 | padding-bottom: 0;
66 | margin-bottom: -5px;
67 | }
68 | .ui.grid.users>.row.th > .column.username {
69 | padding-left: 16px !important;
70 | }
71 | .ui.grid.users>.row.th>.column.actions > label:first-child {
72 | margin-left: 10px;
73 | margin-right: 15px;
74 | }
75 | .ui.grid.users>.row.th>.column.actions > label:nth-child(2) {
76 | margin-right: 20px;
77 | }
78 |
79 | .ui.grid.users>.row > .column:first-child {
80 | width: calc(100% - 360px);
81 | }
82 | .ui.grid.users>.row > .column:last-child {
83 | width: auto;
84 | text-align: right;
85 | }
86 | .ui.grid.users>.row > .column.lastactive {
87 | width: 140px;
88 | margin: 0 15px 1px 15px;
89 | }
90 |
91 | .ui.segment {
92 | box-shadow: none;
93 | -webkit-box-shadow: none;
94 | }
95 | .ui.segment:first-child {
96 | margin-top: 1px;
97 | }
98 |
99 | .ui.grid.users>.row>.column .ui.segment {
100 | padding: 9px 15px;
101 | }
102 |
103 | .ui.grid.users>.row>.column:last-child > .buttons {
104 | margin-right: 12px;
105 | }
106 | .ui.grid.users>.row>.column:last-child i.icon {
107 | margin-right: 0;
108 | pointer-events: none;
109 | }
110 |
--------------------------------------------------------------------------------
/assets/css/c3.min.css:
--------------------------------------------------------------------------------
1 | .c3 svg{font:10px sans-serif;-webkit-tap-highlight-color:transparent}.c3 line,.c3 path{fill:none;stroke:#000}.c3 text{-webkit-user-select:none;-moz-user-select:none;user-select:none}.c3-bars path,.c3-event-rect,.c3-legend-item-tile,.c3-xgrid-focus,.c3-ygrid{shape-rendering:crispEdges}.c3-chart-arc path{stroke:#fff}.c3-chart-arc rect{stroke:#fff;stroke-width:1}.c3-chart-arc text{fill:#fff;font-size:13px}.c3-grid line{stroke:#aaa}.c3-grid text{fill:#aaa}.c3-xgrid,.c3-ygrid{stroke-dasharray:3 3}.c3-text.c3-empty{fill:grey;font-size:2em}.c3-line{stroke-width:1px}.c3-circle{fill:currentColor}.c3-circle._expanded_{stroke-width:1px;stroke:#fff}.c3-selected-circle{fill:#fff;stroke-width:2px}.c3-bar{stroke-width:0}.c3-bar._expanded_{fill-opacity:1;fill-opacity:.75}.c3-target.c3-focused{opacity:1}.c3-target.c3-focused path.c3-line,.c3-target.c3-focused path.c3-step{stroke-width:2px}.c3-target.c3-defocused{opacity:.3!important}.c3-region{fill:#4682b4;fill-opacity:.1}.c3-region text{fill-opacity:1}.c3-brush .extent{fill-opacity:.1}.c3-legend-item{font-size:12px}.c3-legend-item-hidden{opacity:.15}.c3-legend-background{opacity:.75;fill:#fff;stroke:#d3d3d3;stroke-width:1}.c3-title{font:14px sans-serif}.c3-tooltip-container{z-index:10}.c3-tooltip{border-collapse:collapse;border-spacing:0;background-color:#fff;empty-cells:show;-webkit-box-shadow:7px 7px 12px -9px #777;-moz-box-shadow:7px 7px 12px -9px #777;box-shadow:7px 7px 12px -9px #777;opacity:.9}.c3-tooltip tr{border:1px solid #ccc}.c3-tooltip th{background-color:#aaa;font-size:14px;padding:2px 5px;text-align:left;color:#fff}.c3-tooltip td{font-size:13px;padding:3px 6px;background-color:#fff;border-left:1px dotted #999}.c3-tooltip td>span{display:inline-block;width:10px;height:10px;margin-right:6px}.c3-tooltip .value{text-align:right}.c3-area{stroke-width:0;opacity:.2}.c3-chart-arcs-title{dominant-baseline:middle;font-size:1.3em}.c3-chart-arcs .c3-chart-arcs-background{fill:#e0e0e0;stroke:#fff}.c3-chart-arcs .c3-chart-arcs-gauge-unit{fill:#000;font-size:16px}.c3-chart-arcs .c3-chart-arcs-gauge-max{fill:#777}.c3-chart-arcs .c3-chart-arcs-gauge-min{fill:#777}.c3-chart-arc .c3-gauge-value{fill:#000}.c3-chart-arc.c3-target g path{opacity:1}.c3-chart-arc.c3-target.c3-focused g path{opacity:1}.c3-drag-zoom.enabled{pointer-events:all!important;visibility:visible}.c3-drag-zoom.disabled{pointer-events:none!important;visibility:hidden}.c3-drag-zoom .extent{fill-opacity:.1}
--------------------------------------------------------------------------------
/assets/css/docs.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Top headerbar
3 | */
4 | .projectname {
5 | cursor: pointer;
6 | }
7 |
8 | .ui.grid {
9 | width: 40%;
10 | min-width: 1000px;
11 | margin: 20px auto 0 auto;
12 | }
13 |
14 | .ui.grid .row:last-child {
15 | padding-bottom: 100px;
16 | }
17 |
18 | /*
19 | * Markdown fixes
20 | */
21 | .markdown-body {
22 | line-height: 0;
23 | white-space: pre-wrap; /* css-3 */
24 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
25 | white-space: -pre-wrap; /* Opera 4-6 */
26 | white-space: -o-pre-wrap; /* Opera 7 */
27 | word-wrap: break-word; /* Internet Explorer 5.5+ */
28 | font-size: 14px;
29 | font-family: Lato,'Helvetica Neue',Arial,Helvetica,sans-serif;
30 | width: 100%;
31 | }
32 |
33 | .markdown-body table {
34 | display: table;
35 | line-height: initial;
36 | }
37 |
38 | .markdown-body li+li {
39 | margin-top: 0;
40 | }
41 | .markdown-body > ul > li {
42 | line-height: 1.5;
43 | margin-top: 3px;
44 | }
45 | .markdown-body > ul > li > ul > li {
46 | line-height: 0;
47 | }
48 | .markdown-body > ul > li > ul > li > ul {
49 | line-height: 0.8;
50 | }
51 | .markdown-body > ul > li > ul > li > ul:first-child {
52 | margin-top: 15px;
53 | }
54 |
55 | .markdown-body ol:not([start]) {
56 | margin: 12px 0;
57 | }
58 | .markdown-body ol:first-child {
59 | margin-bottom: 32px;
60 | }
61 | .markdown-body ol li {
62 | padding-left: 6px;
63 | line-height: 1.5;
64 | }
65 | .markdown-body ol li>p {
66 | margin-top: -10px;
67 | margin-left: 6px;
68 | line-height: 1.5;
69 | }
70 |
71 | .markdown-body img {
72 | display: block;
73 | margin: 0 auto;
74 | }
75 | .markdown-body img[src$='#wide'] {
76 | width: 100%;
77 | }
78 | .markdown-body img[src$='#left'] {
79 | margin-left: 0;
80 | }
81 |
82 | .markdown-body table tr,
83 | .markdown-body table td,
84 | .markdown-body table th {
85 | border: 0;
86 | line-height: 1.7;
87 | }
88 | .markdown-body table th {
89 | border-bottom: 2px solid #dfe2e5;
90 | text-align: start;
91 | }
92 | .markdown-body table td {
93 | border-bottom: 1px solid #dfe2e5;
94 | }
95 | .markdown-body table tr:nth-child(odd) > td {
96 | background-color: #f6f8fa;
97 | }
98 | .markdown-body table tr:nth-child(even) > td {
99 | background-color: #fff;
100 | }
101 |
102 | .markdown-body blockquote {
103 | border-left: .3em solid #dfe2e5;
104 | }
105 |
106 | .markdown-body code {
107 | padding: 3px 5px 1px 5px;
108 | margin: 0 1px;
109 | color: #ff5f2d;
110 | background-color: #ffecdd;
111 | }
112 | .markdown-body pre code {
113 | color: #24292e;
114 | background-color: initial;
115 | }
116 |
117 | .markdown-body a {
118 | color: #06d;
119 | }
120 |
--------------------------------------------------------------------------------
/assets/css/profile.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Top
3 | */
4 | .ui.grid {
5 | width: 40%;
6 | min-width: 1015px;
7 | margin: 20px auto 0 auto;
8 | }
9 |
10 | .ui.grid h4 {
11 | width: 100%;
12 | padding: 0 0 .5em 0.7em;
13 | margin-bottom: 0;
14 | border-bottom: 1px solid #eaecef;
15 | }
16 |
17 | /*
18 | * Content style
19 | */
20 | .ui.two.column.grid .row .column span {
21 | font-weight: bold;
22 | margin-top: 9px;
23 | position: absolute;
24 | }
25 | .ui.toggle.checkbox {
26 | margin-top: 9px;
27 | }
28 |
29 | .ui.grid>.row.username {
30 | padding-bottom: 2rem;
31 | }
32 | .ui.grid>.row.short {
33 | padding-bottom: 0;
34 | }
35 |
36 | .ui.grid>.row>.column:first-child {
37 | width: 20%;
38 | }
39 | .ui.grid>.row>.column:last-child {
40 | width: 80%;
41 | }
42 |
43 | .row .ui.example {
44 | background-color: #f6f8fa;
45 | width: calc(50% - 5px);
46 | margin: 0;
47 | border: none;
48 | font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
49 | font-size: 85%;
50 | padding: 16px 16px 12px 16px;
51 | -webkit-box-shadow: none;
52 | box-shadow: none;
53 | }
54 | .row .ui.example:first-child {
55 | margin-right: 10px;
56 | }
57 |
58 | .li-list {
59 | line-height: 1.8;
60 | margin-top: 0;
61 | }
62 |
63 | .ui.grid .ui.message {
64 | width: 100%;
65 | -webkit-box-shadow: none;
66 | box-shadow: none;
67 | }
68 |
69 | .ui.message {
70 | margin: 10px 0 0 0;
71 | }
72 | .ui.blue.message a {
73 | color: #2185d0;
74 | font-weight: bold;
75 | }
76 |
77 | .ui[class*="two column"].grid>.row>.column .ui.input {
78 | width: 100%;
79 | }
80 |
81 | .ui.uuid.button {
82 | margin-top: -9px;
83 | margin-left: 10px;
84 | margin-bottom: 5px;
85 | }
86 |
87 | /*
88 | * Upload form
89 | */
90 | .five.fields,
91 | .field .ui.input.left.icon,
92 | .field .ui.dropdown,
93 | .field > .ui.input {
94 | width: 100%;
95 | min-width: 10px;
96 | }
97 | .five.fields > .field {
98 | float: left;
99 | margin-right: 10px;
100 | }
101 | .ui.daterange.button {
102 | width: 100%;
103 | color: rgba(0, 0, 0, .87);
104 | font-weight: normal;
105 | }
106 | .five.fields > .field:nth-child(2) {
107 | width: 400px;
108 | }
109 | .five.fields > .field:nth-child(3) {
110 | width: 105px;
111 | }
112 | .five.fields > .field:nth-child(4) {
113 | width: 200px;
114 | }
115 | .five.fields > .field:last-child {
116 | margin-right: 0;
117 | }
118 |
119 | .five.fields > .field > label {
120 | display: block;
121 | margin-bottom: 5px;
122 | font-weight: bold;
123 | }
124 |
125 | /*
126 | * File selection & conversion
127 | */
128 | #file {
129 | width: 240px;
130 | margin-right: 10px;
131 | }
132 | .ui.action.input input[type="text"] {
133 | cursor: pointer;
134 | border-right: 0;
135 | color: #aaaeae;
136 | }
137 | .ui.action.input input[type="file"] {
138 | display: none;
139 | }
140 |
141 | /*
142 | * Queues
143 | */
144 | #queueHeader,
145 | #downloadsHeader {
146 | display: none;
147 | }
148 |
--------------------------------------------------------------------------------
/assets/css/signin.css:
--------------------------------------------------------------------------------
1 | .ui.grid {
2 | height: 100%;
3 | }
4 |
5 | .column.five.wide img {
6 | width: 150px;
7 | display: block;
8 | margin: 0 auto;
9 | }
10 |
11 | .column.five.wide h1 {
12 | text-align: center;
13 | margin-top: 10px;
14 | font-size: 40px;
15 | }
16 |
17 | .column.five.wide h1 sub {
18 | color: #cccfcf;
19 | }
20 |
--------------------------------------------------------------------------------
/assets/css/themes/basic/assets/fonts/icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/basic/assets/fonts/icons.eot
--------------------------------------------------------------------------------
/assets/css/themes/basic/assets/fonts/icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/basic/assets/fonts/icons.ttf
--------------------------------------------------------------------------------
/assets/css/themes/basic/assets/fonts/icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/basic/assets/fonts/icons.woff
--------------------------------------------------------------------------------
/assets/css/themes/default/assets/fonts/brand-icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/default/assets/fonts/brand-icons.eot
--------------------------------------------------------------------------------
/assets/css/themes/default/assets/fonts/brand-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/default/assets/fonts/brand-icons.ttf
--------------------------------------------------------------------------------
/assets/css/themes/default/assets/fonts/brand-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/default/assets/fonts/brand-icons.woff
--------------------------------------------------------------------------------
/assets/css/themes/default/assets/fonts/brand-icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/default/assets/fonts/brand-icons.woff2
--------------------------------------------------------------------------------
/assets/css/themes/default/assets/fonts/icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/default/assets/fonts/icons.eot
--------------------------------------------------------------------------------
/assets/css/themes/default/assets/fonts/icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/default/assets/fonts/icons.ttf
--------------------------------------------------------------------------------
/assets/css/themes/default/assets/fonts/icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/default/assets/fonts/icons.woff
--------------------------------------------------------------------------------
/assets/css/themes/default/assets/fonts/icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/default/assets/fonts/icons.woff2
--------------------------------------------------------------------------------
/assets/css/themes/default/assets/fonts/outline-icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/default/assets/fonts/outline-icons.eot
--------------------------------------------------------------------------------
/assets/css/themes/default/assets/fonts/outline-icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/default/assets/fonts/outline-icons.ttf
--------------------------------------------------------------------------------
/assets/css/themes/default/assets/fonts/outline-icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/default/assets/fonts/outline-icons.woff
--------------------------------------------------------------------------------
/assets/css/themes/default/assets/fonts/outline-icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/default/assets/fonts/outline-icons.woff2
--------------------------------------------------------------------------------
/assets/css/themes/default/assets/images/flags.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/default/assets/images/flags.png
--------------------------------------------------------------------------------
/assets/css/themes/github/assets/fonts/octicons-local.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/github/assets/fonts/octicons-local.ttf
--------------------------------------------------------------------------------
/assets/css/themes/github/assets/fonts/octicons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/github/assets/fonts/octicons.ttf
--------------------------------------------------------------------------------
/assets/css/themes/github/assets/fonts/octicons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/github/assets/fonts/octicons.woff
--------------------------------------------------------------------------------
/assets/css/themes/material/assets/fonts/icons.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/material/assets/fonts/icons.eot
--------------------------------------------------------------------------------
/assets/css/themes/material/assets/fonts/icons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/material/assets/fonts/icons.ttf
--------------------------------------------------------------------------------
/assets/css/themes/material/assets/fonts/icons.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/material/assets/fonts/icons.woff
--------------------------------------------------------------------------------
/assets/css/themes/material/assets/fonts/icons.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/css/themes/material/assets/fonts/icons.woff2
--------------------------------------------------------------------------------
/assets/fonts/Roboto-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/fonts/Roboto-Bold.ttf
--------------------------------------------------------------------------------
/assets/fonts/Roboto-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/fonts/Roboto-Medium.ttf
--------------------------------------------------------------------------------
/assets/fonts/Roboto-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/fonts/Roboto-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/RobotoMono-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/fonts/RobotoMono-Medium.ttf
--------------------------------------------------------------------------------
/assets/fonts/RobotoMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/fonts/RobotoMono-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/assets/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/assets/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/assets/img/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/architecture.png
--------------------------------------------------------------------------------
/assets/img/autocomplete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/autocomplete.png
--------------------------------------------------------------------------------
/assets/img/cef.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/cef.png
--------------------------------------------------------------------------------
/assets/img/cluster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/cluster.png
--------------------------------------------------------------------------------
/assets/img/datasources.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/datasources.png
--------------------------------------------------------------------------------
/assets/img/filters.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/filters.png
--------------------------------------------------------------------------------
/assets/img/nodes-images.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/nodes-images.png
--------------------------------------------------------------------------------
/assets/img/red-filter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/red-filter.png
--------------------------------------------------------------------------------
/assets/img/results-extended.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/results-extended.png
--------------------------------------------------------------------------------
/assets/img/results-styled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/results-styled.png
--------------------------------------------------------------------------------
/assets/img/results.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/results.png
--------------------------------------------------------------------------------
/assets/img/screen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/screen.png
--------------------------------------------------------------------------------
/assets/img/stats.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/stats.png
--------------------------------------------------------------------------------
/assets/img/ui-demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/ui-demo.gif
--------------------------------------------------------------------------------
/assets/img/ui-elements.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/ui-elements.png
--------------------------------------------------------------------------------
/assets/img/ui-elements.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/ui-elements.xcf
--------------------------------------------------------------------------------
/assets/img/users.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cert-lv/graphoscope/d79d59073b1a5379a50b74d34c4ffb7204554ad2/assets/img/users.png
--------------------------------------------------------------------------------
/assets/js/account.js:
--------------------------------------------------------------------------------
1 | /*
2 | * User's account management
3 | */
4 | class Account {
5 | constructor(profile) {
6 | // Pointer to the profile page core
7 | this.profile = profile;
8 |
9 | // Bind Web GUI buttons
10 | this.bind();
11 | }
12 |
13 | /*
14 | * Bind Web GUI buttons
15 | */
16 | bind() {
17 | // Regenerate UUID
18 | $('.ui.uuid.button').on('click', (e) => {
19 | this.regenerateUUID();
20 | });
21 |
22 | // Save account data
23 | $('.ui.save.account.button').on('click', (e) => {
24 | this.save();
25 | });
26 |
27 | // Delete own account
28 | $('.ui.delete.account.button').on('click', (e) => {
29 | this.profile.websocket.send('account-delete');
30 | window.location.replace('/signin');
31 | });
32 | }
33 |
34 | /*
35 | * Regenerate auth UUID
36 | */
37 | regenerateUUID() {
38 | this.profile.websocket.send('uuid');
39 | }
40 |
41 | /*
42 | * Save account data
43 | */
44 | save() {
45 | const pass = document.getElementById('newPassword').value,
46 | passRepeat = document.getElementById('newPasswordRepeat').value;
47 |
48 | // Compare both passwords
49 | if (pass !== passRepeat) {
50 | this.profile.modal.error('Unsuccessful request!', 'Passwords do not match!');
51 | return;
52 | }
53 |
54 | this.profile.websocket.send('account-save', pass);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/assets/js/admin-actions.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Administrators only actions
3 | */
4 | class AdminActions {
5 | constructor(admin) {
6 | // Pointer to the admin page core
7 | this.admin = admin;
8 |
9 | // Bind Web GUI buttons
10 | this.bind();
11 | }
12 |
13 | /*
14 | * Bind actions to the Web GUI elements
15 | */
16 | bind() {
17 | // Refresh the list of fields to query for the Web GUI autocomplete
18 | $('.ui.reload.button').on('click', (e) => {
19 | this.reloadPlugins();
20 | });
21 | }
22 |
23 | /*
24 | * Reload collectors and processors
25 | */
26 | reloadPlugins() {
27 | this.admin.websocket.send('reload');
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/assets/js/admin.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Admin webpage core.
3 | *
4 | * Allows to set global graph settings and
5 | * manage registered users
6 | */
7 | window.addEventListener('DOMContentLoaded', function() {
8 |
9 | class Admin {
10 | constructor() {
11 | // Prepare modal window first
12 | this.modal = new Modal();
13 |
14 | // Web GUI notifications
15 | this.notifications = new Notifications(this);
16 |
17 | // Websocket connection for the client-server-client communication
18 | this.websocket = new Websocket(this);
19 |
20 | // Global graph settings
21 | this.settings = new Settings(this);
22 |
23 | // Registered users management
24 | this.users = new Users(this);
25 |
26 | // Administrators only actions
27 | this.actions = new AdminActions(this);
28 | }
29 | }
30 |
31 | const admin = new Admin();
32 |
33 | });
34 |
--------------------------------------------------------------------------------
/assets/js/docs.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Built-in documentation webpage core.
3 | * Allows to render Markdown code, one source for each section
4 | */
5 | window.addEventListener('DOMContentLoaded', function() {
6 |
7 | class Docs {
8 | constructor() {
9 | // Prepare modal window first
10 | this.modal = new Modal();
11 |
12 | // Web GUI notifications
13 | this.notifications = new Notifications(this);
14 |
15 | // Init Semantic UI dynamic elements
16 | this.prepareSemantic();
17 |
18 | // Init Markdown rendering engine
19 | const md = window.markdownit().
20 | use(window.markdownItAnchor).
21 | use(window.markdownItTocDoneRight);
22 |
23 | // Render UI docs
24 | var result = md.render(document.getElementById('ui-md').innerText);
25 | document.getElementById('ui-md').innerHTML = result;
26 |
27 | // Render search docs
28 | result = md.render(document.getElementById('search-md').innerText);
29 | document.getElementById('search-md').innerHTML = result;
30 |
31 | // Render administration section
32 | result = md.render(document.getElementById('admin-md').innerText);
33 | document.getElementById('admin-md').innerHTML = result;
34 | }
35 |
36 | /*
37 | * Init Semantic UI dynamic elements
38 | */
39 | prepareSemantic() {
40 | $('.ui.dropdown').dropdown();
41 | $('.ui.secondary.menu .item').tab();
42 | }
43 | }
44 |
45 | const docs = new Docs();
46 |
47 | });
48 |
--------------------------------------------------------------------------------
/assets/js/features.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Notification about new service's features.
3 | * Appears once for each user
4 | */
5 | class Features {
6 | constructor(application) {
7 | // Pointer to the main page core
8 | this.application = application;
9 |
10 | // Show new features modal window
11 | this.show();
12 | }
13 |
14 | /*
15 | * Show new features modal window.
16 | * FEATURES global variable is undefined if current user is already informed
17 | */
18 | show() {
19 | if (typeof FEATURES === 'undefined')
20 | return;
21 |
22 | var list = '
';
23 |
24 | for (var i = 1; i < FEATURES.length; i++)
25 | list += '- ' + FEATURES[i] + '
';
26 |
27 | list += '
Date: ' + FEATURES[0] + '';
28 |
29 | this.application.modal.ok('New features!', list);
30 | }
31 | }
--------------------------------------------------------------------------------
/assets/js/index.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Main webpage core.
3 | *
4 | * Allows to query data sources, create inclusion/exclusion filters,
5 | * manage dashboards, draw graph and statistics info,
6 | * show the latest service's features
7 | */
8 | window.addEventListener('DOMContentLoaded', function() {
9 |
10 | class Application {
11 | constructor() {
12 | // Notifications
13 | this.notifications = new Notifications(this);
14 |
15 | // Datetime range picker
16 | this.calendar = new Calendar();
17 |
18 | // Query data sources
19 | this.search = new Search(this);
20 |
21 | // Nodes counts by groups
22 | this.tags = new Tags(this);
23 |
24 | // User filters to request new data or exclude existing
25 | this.filters = new Filters(this);
26 |
27 | // Dashboards saving & loading features
28 | this.dashboards = new Dashboards(this);
29 |
30 | // Graph canvas
31 | this.graph = new Graph(this);
32 |
33 | // Statistics charts when returned nodes limit is exceeded
34 | this.charts = new Charts(this);
35 |
36 | // Modal window
37 | this.modal = new Modal();
38 |
39 | // Notification about new service's features
40 | this.features = new Features(this);
41 |
42 | // Websocket connection for the client-server-client communication
43 | this.websocket = new Websocket(this);
44 | }
45 | }
46 |
47 | const application = new Application();
48 |
49 | });
50 |
--------------------------------------------------------------------------------
/assets/js/markdownItTocDoneRight.umd.js:
--------------------------------------------------------------------------------
1 | !function(e,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(e=e||self).markdownItTocDoneRight=n()}(this,function(){function e(e){return encodeURIComponent(String(e).trim().toLowerCase().replace(/\s+/g,"-"))}function n(e){return String(e).replace(/&/g,"&").replace(/"/g,""").replace(/'/g,"'").replace(//g,">")}return function(t,r){var l;r=Object.assign({},{placeholder:"(\\$\\{toc\\}|\\[\\[?_?toc_?\\]?\\]|\\$\\)",slugify:e,uniqueSlugStartIndex:1,containerClass:"table-of-contents",containerId:void 0,listClass:void 0,itemClass:void 0,linkClass:void 0,level:1,listType:"ol",format:void 0,callback:void 0},r);var i=new RegExp("^"+r.placeholder+"$","i");t.renderer.rules.tocOpen=function(e,t){var l=Object.assign({},r);return e&&t>=0&&(l=Object.assign(l,e[t].inlineOptions)),""},t.renderer.rules.tocBody=function(e,t){var i=Object.assign({},r);e&&t>=0&&(i=Object.assign(i,e[t].inlineOptions));var o,s={},c=Array.isArray(i.level)?(o=i.level,function(e){return o.includes(e)}):function(e){return function(n){return n>=e}}(i.level);return function e(t){var l=i.listClass?' class="'+n(i.listClass)+'"':"",o=i.itemClass?' class="'+n(i.itemClass)+'"':"",a=i.linkClass?' class="'+n(i.linkClass)+'"':"";if(0===t.c.length)return"";var u="";return(0===t.l||c(t.l))&&(u+="<"+(n(i.listType)+l)+">"),t.c.forEach(function(t){c(t.l)?u+="'+("function"==typeof i.format?i.format(t.n,n):n(t.n))+""+e(t)+"":u+=e(t)}),(0===t.l||c(t.l))&&(u+=""+n(i.listType)+">"),u}(l)},t.core.ruler.push("generateTocAst",function(e){l=function(e){for(var n={l:0,n:"",c:[]},t=[n],r=0,l=e.length;rt[0].l)t[0].c.push(s),t.unshift(s);else if(s.l===t[0].l)t[1].c.push(s),t[0]=s;else{for(;s.l<=t[0].l;)t.shift();t[0].c.push(s),t.unshift(s)}}}return n}(e.tokens),"function"==typeof r.callback&&r.callback(t.renderer.rules.tocOpen()+t.renderer.rules.tocBody()+t.renderer.rules.tocClose(),l)}),t.block.ruler.before("heading","toc",function(e,n,t,r){var l,o=e.src.slice(e.bMarks[n]+e.tShift[n],e.eMarks[n]).split(" ")[0];if(!i.test(o))return!1;if(r)return!0;var s=i.exec(o),c={};if(null!==s&&3===s.length)try{c=JSON.parse(s[2])}catch(e){}return e.line=n+1,(l=e.push("tocOpen","nav",1)).markup="",l.map=[n,e.line],l.inlineOptions=c,(l=e.push("tocBody","",0)).markup="",l.map=[n,e.line],l.inlineOptions=c,l.children=[],(l=e.push("tocClose","nav",-1)).markup="",!0},{alt:["paragraph","reference","blockquote"]})}});
2 | //# sourceMappingURL=markdownItTocDoneRight.umd.js.map
3 |
--------------------------------------------------------------------------------
/assets/js/modal.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Modal window notifications
3 | */
4 | class Modal {
5 | constructor() {
6 | // Pointers to the window HTML elements
7 | this.containerOk = $('.ui.basic.ok.modal');
8 | this.containerEmpty = $('.ui.basic.empty.modal');
9 | }
10 |
11 | /*
12 | * Empty notification without actions
13 | */
14 | empty(header, text, icon) {
15 | //console.log('empty', header, text);
16 |
17 | if (header) {
18 | // Remove previous message if exists
19 | if (!text)
20 | text = '';
21 |
22 | this.containerEmpty.find('.header').html('' + header);
23 | this.containerEmpty.find('.content').html(text);
24 | this.containerEmpty.modal('show');
25 | }
26 | }
27 |
28 | /*
29 | * Notify that everything is Ok
30 | */
31 | ok(header, text) {
32 | //console.log('ok', header, text);
33 |
34 | if (header) {
35 | // Remove previous message if exists
36 | if (!text)
37 | text = '';
38 |
39 | this.containerOk.find('.header').html('' + header);
40 | this.containerOk.find('.content').html(text);
41 | this.containerOk.modal('show');
42 | }
43 | }
44 |
45 | /*
46 | * Notify that something is wrong
47 | */
48 | error(header, text) {
49 | //console.log(header, text);
50 |
51 | this.containerOk.find('.header').html('' + header);
52 | this.containerOk.find('.content').html(text.replace(/^([\w -"']*):/, '$1:'));
53 | this.containerOk.modal('show');
54 | }
55 |
56 | /*
57 | * Close any modal window
58 | */
59 | close() {
60 | this.containerOk.modal('hide');
61 | this.containerEmpty.modal('hide');
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/assets/js/options.js:
--------------------------------------------------------------------------------
1 | /*
2 | * User's personal settings
3 | */
4 | class Options {
5 | constructor(profile) {
6 | // Pointer to the profile page core
7 | this.profile = profile;
8 |
9 | // Init Semantic UI dynamic elements
10 | this.prepareSemantic();
11 |
12 | // Bind Web GUI buttons
13 | this.bind();
14 | }
15 |
16 | /*
17 | * Init Semantic UI dynamic elements
18 | */
19 | prepareSemantic() {
20 | $('.ui.dropdown').dropdown();
21 | $('.ui.secondary.menu .item').tab();
22 | }
23 |
24 | /*
25 | * Bind Web GUI buttons
26 | */
27 | bind() {
28 | // Button to save settings
29 | $('.ui.save.profile.button').on('click', (e) => {
30 | this.save();
31 | });
32 | }
33 |
34 | /*
35 | * Save settings
36 | */
37 | save() {
38 | const options = document.getElementById('stabilization').value + ',' +
39 | document.getElementById('limit').value + ',' +
40 | $('.ui.checkbox.show_limited').checkbox('is checked') + ',' +
41 | $('.ui.checkbox.debug').checkbox('is checked');
42 |
43 | // Send to the server
44 | this.profile.websocket.send('options', options);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/assets/js/profile.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Profile webpage core.
3 | *
4 | * Allows to save personal and account settings, launch long term actions
5 | */
6 | window.addEventListener('DOMContentLoaded', function() {
7 |
8 | class Profile {
9 | constructor() {
10 | // Prepare modal window first
11 | this.modal = new Modal();
12 |
13 | // Notifications
14 | this.notifications = new Notifications(this);
15 |
16 | // Websocket connection for the client-server-client communication
17 | this.websocket = new Websocket(this);
18 |
19 | // User's personal settings management
20 | this.options = new Options(this);
21 |
22 | // User's account management
23 | this.account = new Account(this);
24 |
25 | // Datetime range picker
26 | this.calendar = new Calendar();
27 |
28 | // Long-term user actions
29 | this.actions = new UserActions(this);
30 | }
31 | }
32 |
33 | const profile = new Profile();
34 |
35 | });
36 |
--------------------------------------------------------------------------------
/assets/js/settings.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Global graph UI settings management
3 | */
4 | class Settings {
5 | constructor(profile) {
6 | // Pointer to the admin page core
7 | this.profile = profile;
8 |
9 | // Init Semantic UI dynamic elements
10 | this.prepareSemantic();
11 |
12 | // Bind Web GUI buttons
13 | this.bind();
14 | }
15 |
16 | /*
17 | * Init Semantic UI dynamic elements
18 | */
19 | prepareSemantic() {
20 | $('.ui.dropdown').dropdown();
21 | $('.ui.secondary.menu .item').tab();
22 | }
23 |
24 | /*
25 | * Bind Web GUI buttons
26 | */
27 | bind() {
28 | // Button to save settings
29 | $('.ui.save.settings.button').on('click', (e) => {
30 | this.save();
31 | });
32 | }
33 |
34 | /*
35 | * Save settings
36 | */
37 | save() {
38 | const settings = document.getElementById('nodesize').value + ',' +
39 | document.getElementById('borderwidth').value + ',' +
40 | document.getElementById('bgcolor').value.toLowerCase() + ',' +
41 | document.getElementById('bordercolor').value.toLowerCase() + ',' +
42 | document.getElementById('nodefontsize').value + ',' +
43 | $('.ui.checkbox.shadow').checkbox('is checked') + ',' +
44 | document.getElementById('edgewidth').value + ',' +
45 | document.getElementById('edgecolor').value.toLowerCase() + ',' +
46 | document.getElementById('edgefontsize').value + ',' +
47 | document.getElementById('edgefontcolor').value.toLowerCase() + ',' +
48 | $('.ui.checkbox.arrow').checkbox('is checked') + ',' +
49 | $('.ui.checkbox.smooth').checkbox('is checked') + ',' +
50 | $('.ui.checkbox.hover').checkbox('is checked') + ',' +
51 | $('.ui.checkbox.multiselect').checkbox('is checked') + ',' +
52 | $('.ui.checkbox.hideedgesondrag').checkbox('is checked');
53 |
54 | // Send settings to the server
55 | this.profile.websocket.send('settings', settings);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/assets/js/tags.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Display the amount of nodes of each group
3 | */
4 | class Tags {
5 | constructor(application) {
6 | // Pointer to the main page core
7 | this.application = application;
8 |
9 | // Container of tags HTML elements
10 | this.container = document.getElementById('count');
11 | }
12 |
13 | /*
14 | * Update nodes count for each group
15 | */
16 | update() {
17 | const count = {};
18 |
19 | // Clear old values
20 | this.container.innerHTML = '';
21 |
22 | // Count existing nodes
23 | for (var id in this.application.graph.network.body.nodes) {
24 | const node = this.application.graph.network.body.nodes[id];
25 |
26 | if (!node.options.hidden) {
27 | if (count[node.options.group])
28 | count[node.options.group] += 1;
29 | else
30 | count[node.options.group] = 1;
31 | }
32 | }
33 |
34 | // Add new tag labels
35 | for (var group in count) {
36 | const entry = document.createElement('div');
37 | entry.innerHTML = '' + group + '
' + count[group] + '
';
38 |
39 | this.container.appendChild(entry);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/assets/js/users.js:
--------------------------------------------------------------------------------
1 | /*
2 | * User management.
3 | * Administrators can reset passwords, delete users, etc.
4 | */
5 | class Users {
6 | constructor(admin) {
7 | // Pointer to the admin page core
8 | this.admin = admin;
9 |
10 | // Bind Web GUI buttons
11 | this.bind();
12 |
13 | // Check whether server has returned an error during the page load
14 | this.checkLoadError();
15 | }
16 |
17 | /*
18 | * Bind Web GUI buttons
19 | */
20 | bind() {
21 | // Access 'this' from 'function()'
22 | const users = this;
23 |
24 | // Button to clear user's password
25 | $('.ui.button.reset').on('click', (e) => {
26 | users.apply(e.target.dataset.username, 'reset-password');
27 | });
28 |
29 | // Button to delete account
30 | $('.ui.button.delete').on('click', (e) => {
31 | users.apply(e.target.dataset.username, 'delete');
32 | });
33 |
34 | // Toggle admin rights
35 | $('.ui.checkbox.admin').checkbox({
36 | onChange: function() {
37 | users.apply(this.dataset.username, 'admin-' + this.checked);
38 | }
39 | });
40 | }
41 |
42 | /*
43 | * Check whether server has returned an error during the page load
44 | */
45 | checkLoadError() {
46 | if (MSG !== '')
47 | this.admin.modal.error('Something went wrong!', MSG);
48 | }
49 |
50 | /*
51 | * Modify selected user.
52 | * Receives a user name and an action to apply
53 | */
54 | apply(username, action) {
55 | this.admin.websocket.send('users', username+','+action);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/assets/tmpl/credits.html:
--------------------------------------------------------------------------------
1 | {{ define "credits" }}
2 |
3 |
6 |
7 | {{ end }}
--------------------------------------------------------------------------------
/assets/tmpl/modal.html:
--------------------------------------------------------------------------------
1 | {{ define "modal" }}
2 |
3 |
6 |
7 |
12 |
13 |
14 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Ok!
24 |
25 |
26 |
27 |
28 |
31 |
35 |
36 | {{ end }}
--------------------------------------------------------------------------------
/assets/tmpl/signin.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Graphoscope
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |

15 |
20 |
21 |
22 |
23 | {{ if ne .Error ""}}
24 |
{{ .Error }}
25 | {{ end }}
26 |
27 |
41 |
42 |
43 |
44 |
47 | {{ template "credits" . }}
48 |
49 |
50 |
--------------------------------------------------------------------------------
/assets/tmpl/topbar.html:
--------------------------------------------------------------------------------
1 | {{ define "topbar" }}
2 |
3 |
52 |
53 | {{ end }}
--------------------------------------------------------------------------------
/certs/graphoscope.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDuzCCAqOgAwIBAgIULDVxLlO0RIPOmmCWbpOXjq5rUj4wDQYJKoZIhvcNAQEL
3 | BQAwbDELMAkGA1UEBhMCTFYxDTALBgNVBAgMBFJpZ2ExDTALBgNVBAcMBFJpZ2Ex
4 | EDAOBgNVBAoMB0NFUlQuTFYxEDAOBgNVBAMMB2NlcnQubHYxGzAZBgkqhkiG9w0B
5 | CQEWDGNlcnRAY2VydC5sdjAgFw0yMDAzMTIxMDU3MjlaGA8yMTIwMDIxNzEwNTcy
6 | OVowbDELMAkGA1UEBhMCTFYxDTALBgNVBAgMBFJpZ2ExDTALBgNVBAcMBFJpZ2Ex
7 | EDAOBgNVBAoMB0NFUlQuTFYxEDAOBgNVBAMMB2NlcnQubHYxGzAZBgkqhkiG9w0B
8 | CQEWDGNlcnRAY2VydC5sdjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
9 | AMdPK8cpDtRs2LqtCO7uMvQcQ9rSixoWMysbjEnimWbL8ua3eTmK2zF3fjnkQ5Tz
10 | Ch18A38veheZOGJmCS5laeeff0QCPmOOWlwaLr2q3RUWT7uBnWSthgM2/02RZqb3
11 | 9TPD9ZFUFMp5EEFnkZowNaBg4txJrm4gN1DPHJwAMajASk/q2xaMJsNfwWNJCZM8
12 | xCKLgoI1xEweZv/wWco2y3k5IlBjsX/CtXuWq7SZM+o6LA6W4vGW2t7TQ9Fr+Z7T
13 | 0JjqH538IXwW4iSdOItq9WY0LHLFMz1IPkaK+clo8GB9kS7iEWRtmEx5PAvS0AAJ
14 | QZUGqvShMKaxdOcphNFHq6UCAwEAAaNTMFEwHQYDVR0OBBYEFLD1Dzi3xnasLnOE
15 | c0eY39xqB9rnMB8GA1UdIwQYMBaAFLD1Dzi3xnasLnOEc0eY39xqB9rnMA8GA1Ud
16 | EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIRPX//IeazsGKrEfu3lWBBE
17 | 29IGcGFb6KSoJTqalBvePS+1JIQMVSNvM1A9dyFLEthG5Xsyg+N1A/tP2Xu/8U1Z
18 | GtYUBhriy5RXWSVSrLpd7zbEs2CjfgkSq2TXg/3V5HzxSwZx6XQW3DmXf3NQFxJe
19 | q+VmqeETMUprndRGFFbSarxYuFP0bHP4z3GFCFCBc/Or8cZ+IDHKZAudFN3eW7uD
20 | CxFuLvWrBeuGEV/Xw81git5z1CMMV5+xCOVGLJNSHNvXqCZ8UT0pKZiZt5tyFuC1
21 | VfMOp7gENnN6AdPkq1/7n8pVSp0ixeQ/2oc/5pnR0LoXlIp1cOup4/dYdxJsbwk=
22 | -----END CERTIFICATE-----
23 |
--------------------------------------------------------------------------------
/certs/graphoscope.key:
--------------------------------------------------------------------------------
1 | -----BEGIN RSA PRIVATE KEY-----
2 | MIIEowIBAAKCAQEAx08rxykO1GzYuq0I7u4y9BxD2tKLGhYzKxuMSeKZZsvy5rd5
3 | OYrbMXd+OeRDlPMKHXwDfy96F5k4YmYJLmVp559/RAI+Y45aXBouvardFRZPu4Gd
4 | ZK2GAzb/TZFmpvf1M8P1kVQUynkQQWeRmjA1oGDi3EmubiA3UM8cnAAxqMBKT+rb
5 | Fowmw1/BY0kJkzzEIouCgjXETB5m//BZyjbLeTkiUGOxf8K1e5artJkz6josDpbi
6 | 8Zba3tND0Wv5ntPQmOofnfwhfBbiJJ04i2r1ZjQscsUzPUg+Ror5yWjwYH2RLuIR
7 | ZG2YTHk8C9LQAAlBlQaq9KEwprF05ymE0UerpQIDAQABAoIBAFbRwfwrgm4+S9pl
8 | bbLGyCNV/Kjhdf6TFQ7+HQpCTxhcVx7xZTkPp5PQvYdyS44ioJFfaBaLE+AbulgC
9 | opU3T/65l7KEV7D+XZYpQZsVRuDcqza+q1Uj0XCtEGE1qUWqVYGLJvl7auMYAWC8
10 | QMytm26VRb03y2flWLM2xPufigI7m/NVMm0pOgEJLaDD3zGu1jM46rsGvfa2daln
11 | g4VdVBDiDopjbVI6GE5Dl6xoc3lB5o+tgMDYLD3cn+WJd2i1I+TuHsBmvLpOXp16
12 | qs8Kf06Apy2xvHUnB6u5NXLn9TxdWf8Jgudqw85ljsH+qif5141VXVfU/gjOPfd2
13 | ZpsbPTECgYEA8KY22nG4a4d48hQrq4czfEP0Ah29bpvFVHCpQDaZbb0iHfVJ7ORT
14 | cTREWTETGBVZgxPICBbwAb633Li9cZExJODD0ZNGUjKoAIaj3TiCUOsrYl2PAKXb
15 | 6qNTcd2x1leuu7PQGEFobFcJi+/nxponWu2f6lNEXjZ8ZPZYTTvlvF8CgYEA1AXg
16 | koobg3j+mmuLs7mgQZLqTnZ2mqcW/2gZCQhvC8DXbPuibt1LzhXuKFTSHffo4DRX
17 | jKNC1aC6G+48bCCa/USPcOJhLwNRB5qpnc94tXlG7535PHcuX3Mk3ksUUzUVXizz
18 | d38U/0FJsR4OubVVTBBrNk6gdHruBBan/BAKFnsCgYAFSpJQMUnty1fEct8W8W0X
19 | YWMfHMpKgVBQb/24tLqg6BS09ey/MbIH/i82itaxo96I/ElcrCxwzWG7j7BSq++Z
20 | sPt9QzC7o/N/t3Yo6hIrd1BH5Gi9iegQ+7BdA5Pic6Ea7XQ45E9Ieo1yLz84ZbFR
21 | 1YG7pEMPk0Ee8y+z2wpNHwKBgFbkrbwA4/PG47mPt+qJefdF6ccMX+FT92XnWNNN
22 | 5IzRlLhyjIiZI1crv7ZBxPdJQeSZLwRRaLO6smt+AL9jwYFo1syxypiE6HGQXlFx
23 | 1Quyz3KmsJ2qTpQJ0aNU69iKGd7F12Yy6/0M2dG/+tL7USDiXb4dDT+PnfqI+oGg
24 | ZTH/AoGBAM8k86vdY8JsXRYGfTvpsNPS/v21TtF1a93WvvOxTTFQQ/HZiXn1MD4D
25 | GFmNdA52Fq2cZkqzKyHjf+1aqPnmBFjEp1527GsOxJ6d/yN2SL3sEgutpblcQk5I
26 | SQh0jE7jLkR8sQE80ARFUC2UYsWI0joU5nJY12nkuXUX28ijE4MU
27 | -----END RSA PRIVATE KEY-----
28 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/rs/zerolog"
8 | yaml "gopkg.in/yaml.v3"
9 | )
10 |
11 | /*
12 | * Structure to store all the service settings.
13 | * Check "graphoscope.yaml.example" file for a detailed all fields description
14 | */
15 | type Config struct {
16 | Server *struct {
17 | Host string `yaml:"host"`
18 | Port string `yaml:"port"`
19 | CertFile string `yaml:"certFile"`
20 | KeyFile string `yaml:"keyFile"`
21 | ReadTimeout int `yaml:"readTimeout"`
22 | ReadHeaderTimeout int `yaml:"readHeaderTimeout"`
23 | } `yaml:"server"`
24 |
25 | Environment string `yaml:"environment"`
26 | Definitions string `yaml:"definitions"`
27 | Plugins string `yaml:"plugins"`
28 | Limit int `yaml:"limit"`
29 | StabilizationTime int `yaml:"stabilizationTime"`
30 |
31 | Log *struct {
32 | File string `yaml:"file"`
33 | MaxSize int `yaml:"maxSize"`
34 | MaxBackups int `yaml:"maxBackups"`
35 | MaxAge int `yaml:"maxAge"`
36 | Level zerolog.Level `yaml:"level"`
37 | } `yaml:"log"`
38 |
39 | Upload *struct {
40 | Path string `yaml:"path"`
41 | MaxSize int64 `yaml:"maxSize"`
42 | MaxIndicators int `yaml:"maxIndicators"`
43 | DeleteInterval int `yaml:"deleteInterval"`
44 | DeleteExpiration int `yaml:"deleteExpiration"`
45 | } `yaml:"upload"`
46 |
47 | Groups string `yaml:"groups"`
48 | Formats string `yaml:"formats"`
49 | Features string `yaml:"features"`
50 | Docs string `yaml:"docs"`
51 |
52 | Database struct {
53 | URL string `yaml:"url"`
54 | Name string `yaml:"name"`
55 | User string `yaml:"user"`
56 | Password string `yaml:"password"`
57 | Users string `yaml:"users"`
58 | Dashboards string `yaml:"dashboards"`
59 | Notes string `yaml:"notes"`
60 | Sessions string `yaml:"sessions"`
61 | Cache string `yaml:"cache"`
62 | Settings string `yaml:"settings"`
63 | Timeout int `yaml:"timeout"`
64 | CacheTTL int32 `yaml:"cacheTTL"`
65 | } `yaml:"database"`
66 |
67 | Sessions *struct {
68 | TTL int `yaml:"ttl"`
69 | CookieName string `yaml:"cookieName"`
70 | AuthenticationKey string `yaml:"authenticationKey"`
71 | EncryptionKey string `yaml:"encryptionKey"`
72 | } `yaml:"sessions"`
73 | }
74 |
75 | /*
76 | * Load configuration from a YAML file.
77 | *
78 | * Service searches for the "./graphoscope.yaml" file by default.
79 | * however, "CONFIG" environment variable can be set to use a different file
80 | */
81 | func loadConfig() error {
82 | path := "graphoscope.yaml"
83 |
84 | if os.Getenv("CONFIG") != "" {
85 | path = os.Getenv("CONFIG")
86 | }
87 |
88 | buffer, err := loadFileIntoString(path)
89 | if err != nil {
90 | return fmt.Errorf("Failed to open configuration file '%s': %s", path, err.Error())
91 | }
92 |
93 | err = yaml.Unmarshal([]byte(buffer), &config)
94 | if err != nil {
95 | return fmt.Errorf("Invalid configuration YAML file '%s': %s", path, err.Error())
96 | }
97 |
98 | return nil
99 | }
100 |
--------------------------------------------------------------------------------
/definitions/outputs/output.yaml.example:
--------------------------------------------------------------------------------
1 | # Name of the data output
2 | name: application
3 |
4 | # Plugin to use
5 | plugin: example
6 |
7 | # Acceptable actions (connecting, search, etc.) timeout.
8 | # String type, not integer. 60s if not specified
9 | timeout: 60s
10 |
--------------------------------------------------------------------------------
/definitions/processors/processor.yaml.example:
--------------------------------------------------------------------------------
1 | # Name of the data processor
2 | name: application
3 |
4 | # Plugin to use
5 | plugin: taxonomy
6 |
7 | # Acceptable actions (connecting, search, etc.) timeout.
8 | # String type, not integer. 60s if not specified
9 | timeout: 60s
10 |
11 |
12 | # Unique data needed by the current processor.
13 | # Can be a map of any types, as plugins can be very different
14 | data:
15 | ...
16 |
--------------------------------------------------------------------------------
/definitions/sources/demo.yaml.example:
--------------------------------------------------------------------------------
1 | name: demo
2 | label: Demo
3 | icon: database
4 |
5 | plugin: file-csv
6 | inGlobal: false
7 | includeDatetime: false
8 | supportsSQL: true
9 |
10 | access:
11 | path: files/demo.csv
12 |
13 |
14 | relations:
15 | -
16 | from:
17 | id: name
18 | group: name
19 | search: name
20 | attributes: [ "age", "language" ]
21 |
22 | to:
23 | id: country
24 | group: country
25 | search: country
26 |
27 | edge:
28 | label: lives in
29 | attributes: []
30 |
31 | -
32 | from:
33 | id: name
34 | group: name
35 | search: name
36 |
37 | to:
38 | id: friend
39 | group: name
40 | search: name
41 |
42 | -
43 | from:
44 | id: language
45 | group: language
46 | search: language
47 |
48 | to:
49 | id: name
50 | group: name
51 | search: name
--------------------------------------------------------------------------------
/docs.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "net/http"
7 | "os"
8 | )
9 |
10 | var (
11 | // Built-in documentation content, N sections
12 | docs [3]string
13 | )
14 |
15 | /*
16 | * Serve '/docs' page with a built-in documentation
17 | */
18 | func docsHandler(w http.ResponseWriter, r *http.Request) {
19 | // Get client's IP
20 | ip, _, err := net.SplitHostPort(r.RemoteAddr)
21 | if err != nil {
22 | log.Error().Msg("Can't get IP for the docs page: " + err.Error())
23 | return
24 | }
25 |
26 | // Check existing session
27 | username, err := sessions.exists(w, r)
28 | if err != nil {
29 | log.Error().
30 | Str("ip", ip).
31 | Msg(err.Error())
32 |
33 | http.Redirect(w, r, "/signin", http.StatusSeeOther)
34 | return
35 | }
36 |
37 | // Get account from a database
38 | account, err := db.getAccount(username)
39 | if err != nil {
40 | log.Error().
41 | Str("ip", ip).
42 | Str("username", username).
43 | Msg("Can't GetAccount for the docs page: " + err.Error())
44 |
45 | http.Redirect(w, r, "/signin", http.StatusSeeOther)
46 | return
47 | }
48 |
49 | // All are admins in service's DEV mode
50 | if config.Environment != "prod" {
51 | account.Admin = true
52 | }
53 |
54 | templateData := &TemplateData{
55 | Account: account,
56 | Docs: docs,
57 | }
58 |
59 | renderTemplate(w, "docs", templateData, nil)
60 |
61 | log.Info().
62 | Str("ip", ip).
63 | Str("username", username).
64 | Msg("Docs page requested")
65 | }
66 |
67 | /*
68 | * Load all documentation from the markdown files.
69 | * One file for one documentation section
70 | */
71 | func loadDocs() error {
72 | for i, name := range []string{"ui", "search", "admin"} {
73 | md, err := loadDoc(name)
74 | if err != nil {
75 | return err
76 | }
77 |
78 | docs[i] = md
79 | }
80 |
81 | log.Debug().Msg("Documentation is parsed")
82 | return nil
83 | }
84 |
85 | /*
86 | * Load documentation from a specific markdown file.
87 | * Receives a name of file to load, its extension has to be ".md"
88 | */
89 | func loadDoc(name string) (string, error) {
90 | mdFile, err := os.Open(config.Docs + "/" + name + ".md")
91 | if err != nil {
92 | return "", fmt.Errorf("Failed to open '%s/%s.md' doc: %s", config.Docs, name, err.Error())
93 | }
94 |
95 | fi, _ := mdFile.Stat()
96 | buffer := make([]byte, fi.Size())
97 | _, err = mdFile.Read(buffer)
98 | if err != nil {
99 | return "", fmt.Errorf("Failed to read '%s/%s.md' doc: %s", config.Docs, name, err.Error())
100 | }
101 |
102 | return string(buffer), nil
103 | }
104 |
--------------------------------------------------------------------------------
/features.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 |
6 | yaml "gopkg.in/yaml.v3"
7 | )
8 |
9 | var (
10 | // A list of new features for the current service's version.
11 | // Will be displayed once for each user
12 | features = []string{}
13 | )
14 |
15 | /*
16 | * Load new features from a YAML file
17 | */
18 | func loadFeatures() error {
19 | buffer, err := loadFileIntoString(config.Features)
20 | if err != nil {
21 | return fmt.Errorf("Failed to read new features file '%s': %s", config.Features, err.Error())
22 | }
23 |
24 | err = yaml.Unmarshal([]byte(buffer), &features)
25 | if err != nil {
26 | return fmt.Errorf("Failed unmarshalling new features yaml: %s", err.Error())
27 | }
28 |
29 | if len(features) > 0 {
30 | log.Debug().Msgf("New features loaded: %v", features)
31 | }
32 |
33 | return nil
34 | }
35 |
36 | /*
37 | * Prevent future notifications after the first one
38 | */
39 | func (a *Account) hideFeatures() {
40 | if a.SeenFeatures == features[0] {
41 | log.Debug().
42 | Str("username", a.Username).
43 | Msg("No new features notifications to disable")
44 | return
45 | }
46 |
47 | err := a.update("seenFeatures", features[0])
48 | if err != nil {
49 | log.Error().Msg("Can't update account to hide new features notifications: " + err.Error())
50 | return
51 | }
52 |
53 | log.Debug().
54 | Str("username", a.Username).
55 | Msg("New features notifications are hidden")
56 | }
57 |
--------------------------------------------------------------------------------
/files/demo.csv:
--------------------------------------------------------------------------------
1 | name,country,age,friend,language
2 | John,Canada,25,Kate,en
3 | John,Canada,25,Jennifer,
4 | Kate,USA,23,Monica,es
5 | Kate,USA,23,John,es
6 | Monica,Canada,35,Kate,
7 | Ben,Japan,41,Kate,en
8 | Ben,Japan,41,John,fr
9 | Ben,Japan,41,Chin,en
10 | Laura,Italy,11,Polina,fr
11 | Polina,Italy,12,Laura,en
12 | Jennifer,Germany,25,John,fr
13 | Sofy,Sweden,51,Tom,en
14 | Tom,Sweden,55,Sofy,fr
15 | Tom,Sweden,55,Chin,fr
16 | Chin,China,61,Tom,
17 | Chin,China,61,Ben,en
--------------------------------------------------------------------------------
/files/features.yaml:
--------------------------------------------------------------------------------
1 | [
2 | "20.12.2024",
3 |
4 | "An option to show partial search results when limit exceeded",
5 | "Latest plugin: Shodan"
6 | ]
--------------------------------------------------------------------------------
/files/formats.yaml.example:
--------------------------------------------------------------------------------
1 | # Query formatting rules.
2 | # Syntax:
3 | #
4 | # indicator's group:
5 | # - regexp 1 to detect it
6 | # - regexp 2 to detect it
7 | # - ...
8 |
9 | ip:
10 | # IPv4
11 | - ^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$
12 | # IPv6
13 | - "^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}\
14 | |((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa\
15 | -f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0\
16 | -4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-\
17 | Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:\
18 | ))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2\
19 | [0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){\
20 | 2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\
21 | \\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){\
22 | 1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d\
23 | |[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0\
24 | -4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$"
25 |
26 | domain:
27 | - ^([\%\w-]+\.)+[\%a-zA-Z]+$
28 |
29 | email:
30 | - ^[\%\w\.+-]+@[\%\w\.]+\.[\%a-zA-Z]{1,9}$
--------------------------------------------------------------------------------
/files/graphoscope.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Graph project
3 |
4 | [Service]
5 | #User=graphoscope
6 | Environment=CONFIG=/etc/graphoscope/graphoscope.yaml
7 | WorkingDirectory=/opt/graphoscope
8 | ExecStart=/opt/graphoscope/graphoscope
9 |
10 | [Install]
11 | WantedBy=multi-user.target
--------------------------------------------------------------------------------
/files/groups.json.example:
--------------------------------------------------------------------------------
1 | {
2 | "ip": {
3 | "shape": "dot",
4 | "color": {
5 | "background": "#ccc",
6 | "border": "#aaa"
7 | }
8 | },
9 | "domain": {
10 | "shape": "dot",
11 | "color": {
12 | "background": "#09f",
13 | "border": "#07d"
14 | },
15 | "font": {
16 | "color": "#05b"
17 | }
18 | },
19 | "email": {
20 | "shape": "icon",
21 | "icon": {
22 | "face": "Icons",
23 | "weight": "bold",
24 | "code": "\uf0e0",
25 | "size": "30",
26 | "color": "#0a6"
27 | },
28 | "font": {
29 | "color": "#062"
30 | }
31 | },
32 | "identifier": {
33 | "shape": "diamond",
34 | "color": {
35 | "background": "#f75",
36 | "border": "#b44"
37 | },
38 | "font": {
39 | "color": "#b44"
40 | }
41 | },
42 | "institution": {
43 | "shape": "square",
44 | "color": {
45 | "background": "#c7e",
46 | "border": "#95b"
47 | },
48 | "font": {
49 | "color": "#84a"
50 | }
51 | },
52 | "person": {
53 | "shape": "dot",
54 | "color": {
55 | "background": "#0c8",
56 | "border": "#084"
57 | },
58 | "font": {
59 | "color": "#084"
60 | }
61 | },
62 | "taxonomy": {
63 | "shape": "square",
64 | "color": {
65 | "background": "#08e",
66 | "border": "#05b"
67 | },
68 | "font": {
69 | "color": "#05b"
70 | }
71 | },
72 | "rtir": {
73 | "shape": "triangle",
74 | "color": {
75 | "background": "#fa5",
76 | "border": "#c70"
77 | },
78 | "font": {
79 | "color": "#a50"
80 | }
81 | },
82 | "application": {
83 | "shape": "hexagon",
84 | "color": {
85 | "background": "#fc5",
86 | "border": "#ca2"
87 | }
88 | },
89 | "md5": {
90 | "shape": "icon",
91 | "icon": {
92 | "face": "Icons",
93 | "weight": "bold",
94 | "code": "\uf0fd",
95 | "size": "30",
96 | "color": "#f65"
97 | },
98 | "font": {
99 | "color": "#833"
100 | }
101 | },
102 | "sha1": {
103 | "shape": "icon",
104 | "icon": {
105 | "face": "Icons",
106 | "weight": "bold",
107 | "code": "\uf0fd",
108 | "size": "30",
109 | "color": "#58f"
110 | },
111 | "font": {
112 | "color": "#037"
113 | }
114 | },
115 | "sha256": {
116 | "shape": "icon",
117 | "icon": {
118 | "face": "Icons",
119 | "weight": "bold",
120 | "code": "\uf0fd",
121 | "size": "30",
122 | "color": "#a7f"
123 | },
124 | "font": {
125 | "color": "#537"
126 | }
127 | },
128 | "cluster": {
129 | "shape": "hexagon",
130 | "size": 25,
131 | "color": {
132 | "background": "#777b7b",
133 | "border": "#566"
134 | },
135 | "font": {
136 | "color": "#ccc"
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/filters.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | )
6 |
7 | /*
8 | * Structure to hold search filter's state
9 | */
10 | type Filter struct {
11 | // When filter is enabled - related request will be launched
12 | // to retrieve the data when web page is loaded
13 | Enabled bool `json:"enabled" bson:"enabled"`
14 |
15 | // User's original query
16 | Query string `json:"query" bson:"query"`
17 | }
18 |
19 | /*
20 | * Handle 'filters' websocket command to save all current user's filters
21 | */
22 | func (a *Account) filtersHandler(filters string) {
23 | var list []map[string]*Filter
24 | err := json.Unmarshal([]byte(filters), &list)
25 | if err != nil {
26 | a.send("error", "Can't parse filters data.", "Can't save filters!")
27 |
28 | log.Error().
29 | Str("ip", a.Session.IP).
30 | Str("username", a.Username).
31 | Msg("Can't unmarshal filters data: " + err.Error())
32 | return
33 | }
34 |
35 | err = a.update("filters", list)
36 | if err != nil {
37 | a.send("error", err.Error(), "Can't save filters!")
38 | return
39 | }
40 |
41 | log.Debug().
42 | Str("ip", a.Session.IP).
43 | Str("username", a.Username).
44 | Msg("Filters are saved")
45 | }
46 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/cert-lv/graphoscope
2 |
3 | go 1.23
4 |
5 | toolchain go1.23.1
6 |
7 | require (
8 | github.com/0xrawsec/golang-utils v1.3.2
9 | github.com/Jeffail/gabs/v2 v2.7.0
10 | github.com/blastrain/vitess-sqlparser v0.0.0-20201030050434-a139afbb1aba
11 | github.com/elastic/go-elasticsearch/v7 v7.17.10
12 | github.com/elastic/go-elasticsearch/v8 v8.15.0
13 | github.com/georgysavva/scany v1.2.2
14 | github.com/go-sql-driver/mysql v1.8.1
15 | github.com/google/uuid v1.6.0
16 | github.com/gorilla/securecookie v1.1.2
17 | github.com/gorilla/sessions v1.4.0
18 | github.com/gorilla/websocket v1.5.3
19 | github.com/jackc/pgx/v4 v4.18.3
20 | github.com/mattn/go-sqlite3 v1.14.22
21 | github.com/mithrandie/csvq-driver v1.7.0
22 | github.com/ns3777k/go-shodan/v4 v4.2.0
23 | github.com/olekukonko/tablewriter v0.0.5
24 | github.com/redis/go-redis/v9 v9.7.0
25 | github.com/rs/zerolog v1.33.0
26 | github.com/umpc/go-sortedmap v0.0.0-20180422175548-64ab94c482f4
27 | github.com/yukithm/json2csv v0.1.2
28 | go.mongodb.org/mongo-driver v1.17.1
29 | golang.org/x/crypto v0.31.0
30 | golang.org/x/sync v0.10.0
31 | gopkg.in/natefinch/lumberjack.v2 v2.2.1
32 | gopkg.in/yaml.v3 v3.0.1
33 | )
34 |
35 | require (
36 | filippo.io/edwards25519 v1.1.0 // indirect
37 | github.com/cespare/xxhash/v2 v2.3.0 // indirect
38 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
39 | github.com/elastic/elastic-transport-go/v8 v8.6.0 // indirect
40 | github.com/go-logr/logr v1.4.1 // indirect
41 | github.com/go-logr/stdr v1.2.2 // indirect
42 | github.com/golang/snappy v0.0.4 // indirect
43 | github.com/google/go-querystring v1.0.0 // indirect
44 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect
45 | github.com/jackc/pgconn v1.14.3 // indirect
46 | github.com/jackc/pgio v1.0.0 // indirect
47 | github.com/jackc/pgpassfile v1.0.0 // indirect
48 | github.com/jackc/pgproto3/v2 v2.3.3 // indirect
49 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
50 | github.com/jackc/pgtype v1.14.3 // indirect
51 | github.com/jackc/puddle v1.3.0 // indirect
52 | github.com/juju/errors v1.0.0 // indirect
53 | github.com/klauspost/compress v1.17.11 // indirect
54 | github.com/mattn/go-colorable v0.1.13 // indirect
55 | github.com/mattn/go-isatty v0.0.20 // indirect
56 | github.com/mattn/go-runewidth v0.0.16 // indirect
57 | github.com/mitchellh/go-homedir v1.1.0 // indirect
58 | github.com/mithrandie/csvq v1.18.1 // indirect
59 | github.com/mithrandie/go-file/v2 v2.1.0 // indirect
60 | github.com/mithrandie/go-text v1.6.0 // indirect
61 | github.com/mithrandie/ternary v1.1.1 // indirect
62 | github.com/montanaflynn/stats v0.7.1 // indirect
63 | github.com/rivo/uniseg v0.4.7 // indirect
64 | github.com/xdg-go/pbkdf2 v1.0.0 // indirect
65 | github.com/xdg-go/scram v1.1.2 // indirect
66 | github.com/xdg-go/stringprep v1.0.4 // indirect
67 | github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
68 | go.opentelemetry.io/otel v1.24.0 // indirect
69 | go.opentelemetry.io/otel/metric v1.24.0 // indirect
70 | go.opentelemetry.io/otel/trace v1.24.0 // indirect
71 | golang.org/x/sys v0.28.0 // indirect
72 | golang.org/x/term v0.27.0 // indirect
73 | golang.org/x/text v0.21.0 // indirect
74 | )
75 |
--------------------------------------------------------------------------------
/gui.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 | )
7 |
8 | var (
9 | // Nodes styling definition.
10 | // Will be applied to the JavaScript engine
11 | groups string
12 | )
13 |
14 | /*
15 | * Setup Web GUI handlers
16 | */
17 | func setupGUI() error {
18 | var err error
19 |
20 | // Parse graph elements style groups definitions
21 | groups, err = loadFileIntoString(config.Groups)
22 | if err != nil {
23 | return errors.New("Can't load groups file: " + err.Error())
24 | }
25 |
26 | // Load query formatting rules,
27 | // which help to format comma/space separated indicators to a valid SQL query
28 | err = loadFormats()
29 | if err != nil {
30 | return errors.New("Can't load query formatting rules: " + err.Error())
31 | }
32 |
33 | // Create a sessions store
34 | err = setupSessions()
35 | if err != nil {
36 | return errors.New("Can't setup sessions store: " + err.Error())
37 | }
38 |
39 | // Parse documentation files
40 | err = loadDocs()
41 | if err != nil {
42 | return errors.New("Can't load documentation: " + err.Error())
43 | }
44 |
45 | // Notify users about new features
46 | err = loadFeatures()
47 | if err != nil {
48 | return errors.New("Can't show new features: " + err.Error())
49 | }
50 |
51 | // Setup the indicators upload feature
52 | err = setupUpload()
53 | if err != nil {
54 | return errors.New("Can't setup the indicators upload feature: " + err.Error())
55 | }
56 |
57 | // Setup additional HTTPS handlers
58 | // which are required by a Web GUI
59 | http.HandleFunc("/", indexHandler)
60 | http.HandleFunc("/signin", signinHandler)
61 | http.HandleFunc("/signup", signupHandler)
62 | http.HandleFunc("/signout", signoutHandler)
63 | http.HandleFunc("/profile", profileHandler)
64 | http.HandleFunc("/admin", adminHandler)
65 | http.HandleFunc("/docs", docsHandler)
66 | http.HandleFunc("/upload", uploadHandler)
67 | http.HandleFunc("/download", downloadHandler)
68 | http.HandleFunc("/ws", wsHandler)
69 |
70 | http.Handle("/assets/", http.StripPrefix("/assets", http.FileServer(http.Dir("assets"))))
71 |
72 | return nil
73 | }
74 |
--------------------------------------------------------------------------------
/loaders.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "regexp"
9 |
10 | yaml "gopkg.in/yaml.v3"
11 | )
12 |
13 | var (
14 | // Query formatting rules,
15 | // which help to format comma/space separated indicators to a valid SQL query
16 | formats string
17 | )
18 |
19 | /*
20 | * Return content of the requested file by its path
21 | */
22 | func loadFileIntoString(path string) (string, error) {
23 | file, err := ioutil.ReadFile(path)
24 | if err != nil {
25 | return "", err
26 | }
27 |
28 | return string(file), nil
29 | }
30 |
31 | /*
32 | * Load query formatting rules
33 | */
34 | func loadFormats() error {
35 | buffer, err := loadFileIntoString(config.Formats)
36 | if err != nil {
37 | return fmt.Errorf("Failed to read rules file '%s': %s", config.Formats, err.Error())
38 | }
39 |
40 | var f map[string][]string
41 |
42 | err = yaml.Unmarshal([]byte(buffer), &f)
43 | if err != nil {
44 | return fmt.Errorf("Failed unmarshalling rules yaml: %s", err.Error())
45 | }
46 |
47 | // Validate regexps
48 | for group, res := range f {
49 | for _, re := range res {
50 | _, err = regexp.Compile(re)
51 | if err != nil {
52 | return fmt.Errorf("Invalid %s's regular expression '%s' : %s", group, re, err.Error())
53 | }
54 | }
55 | }
56 |
57 | b, err := json.Marshal(f)
58 | if err != nil {
59 | return fmt.Errorf("Failed to marshal rules struct: %s", err.Error())
60 | }
61 |
62 | formats = string(b)
63 | return nil
64 | }
65 |
66 | /*
67 | * Load service's version
68 | */
69 | func loadVersion() error {
70 | path := "VERSION"
71 | var err error
72 |
73 | // Try to get from the environment variable first
74 | if os.Getenv(path) != "" {
75 | version = os.Getenv(path)
76 | return nil
77 | }
78 |
79 | version, err = loadFileIntoString(path)
80 | if err != nil {
81 | return err
82 | }
83 |
84 | return nil
85 | }
86 |
--------------------------------------------------------------------------------
/logger.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "time"
6 |
7 | "github.com/rs/zerolog"
8 | "gopkg.in/natefinch/lumberjack.v2"
9 | )
10 |
11 | /*
12 | * Setup logger to the file and stdout.
13 | *
14 | * In a production environment log events to the file only,
15 | * in a development environment log to the stdout only
16 | */
17 | func setupLogger() error {
18 |
19 | // For the production server
20 | if config.Environment == "prod" {
21 | // Lumberjack provides log files rotation
22 | log = zerolog.New(&lumberjack.Logger{
23 | Filename: config.Log.File,
24 | MaxSize: config.Log.MaxSize, // Size in MB before file gets rotated
25 | MaxBackups: config.Log.MaxBackups, // Max number of files kept before being overwritten
26 | MaxAge: config.Log.MaxAge, // Max number of days to keep the files
27 | Compress: true, // Whether to compress log files using gzip
28 | }).With().Timestamp().Logger()
29 |
30 | zerolog.SetGlobalLevel(config.Log.Level)
31 |
32 | return nil
33 | }
34 |
35 | // For the development
36 | stdout := zerolog.ConsoleWriter{
37 | Out: os.Stdout,
38 | TimeFormat: time.RFC3339,
39 | }
40 |
41 | log = zerolog.New(stdout).With().Timestamp().Logger()
42 | zerolog.SetGlobalLevel(config.Log.Level)
43 |
44 | return nil
45 | }
46 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "crypto/tls"
5 | "fmt"
6 | "net/http"
7 | "os"
8 | "time"
9 |
10 | //_ "net/http/pprof"
11 |
12 | "github.com/rs/zerolog"
13 | )
14 |
15 | var (
16 | // Holder all service's configuration
17 | config *Config
18 |
19 | // Instance of the global logger
20 | log zerolog.Logger
21 |
22 | // Current service's version
23 | version string
24 | )
25 |
26 | /*
27 | * Use recommended TLS settings
28 | */
29 | func setupTLSserver() *http.Server {
30 | cfg := &tls.Config{
31 | MinVersion: tls.VersionTLS12, // At least TLS v1.2 is recommended
32 | }
33 |
34 | // Enable secure ciphers only
35 | for _, cipherSuite := range tls.CipherSuites() {
36 | cfg.CipherSuites = append(cfg.CipherSuites, cipherSuite.ID)
37 | }
38 |
39 | return &http.Server{
40 | Addr: config.Server.Host + ":" + config.Server.Port,
41 | TLSConfig: cfg,
42 | ReadTimeout: time.Duration(config.Server.ReadTimeout) * time.Second,
43 | ReadHeaderTimeout: time.Duration(config.Server.ReadHeaderTimeout) * time.Second,
44 | }
45 | }
46 |
47 | func main() {
48 | /*
49 | * Parse configuration file
50 | */
51 | err := loadConfig()
52 | if err != nil {
53 | fmt.Fprintf(os.Stderr, "Can't load configuration: %s", err.Error())
54 | os.Exit(1)
55 | }
56 |
57 | /*
58 | * Setup a global logger to the file or stdout
59 | */
60 | err = setupLogger()
61 | if err != nil {
62 | fmt.Fprintf(os.Stderr, "Can't setup a logger: %s", err.Error())
63 | os.Exit(1)
64 | }
65 |
66 | /*
67 | * Setup a database
68 | */
69 | err = setupDatabase()
70 | if err != nil {
71 | log.Fatal().Msg("Can't setup a database: " + err.Error())
72 | }
73 |
74 | /*
75 | * Setup Web GUI handlers
76 | */
77 | err = setupGUI()
78 | if err != nil {
79 | log.Fatal().Msg("Can't start Web GUI components: " + err.Error())
80 | }
81 |
82 | /*
83 | * Load plugins
84 | */
85 | err = loadPlugins()
86 | if err != nil {
87 | log.Fatal().Msg("Can't load plugins: " + err.Error())
88 | }
89 |
90 | /*
91 | * Setup collectors for the predefined data sources
92 | */
93 | err = setupCollectors()
94 | if err != nil {
95 | log.Fatal().Msg("Can't load collectors: " + err.Error())
96 | }
97 |
98 | /*
99 | * Setup processors of the data sources received data
100 | */
101 | err = setupProcessors()
102 | if err != nil {
103 | log.Fatal().Msg("Can't load processors: " + err.Error())
104 | }
105 |
106 | /*
107 | * Stop collectors on service exit
108 | */
109 | defer func() {
110 | for name, collector := range collectors {
111 | err := collector.Stop()
112 |
113 | if err != nil {
114 | log.Error().
115 | Str("source", name).
116 | Msg("Can't stop the collector: " + err.Error())
117 | } else {
118 | log.Debug().
119 | Str("source", name).
120 | Msg("Collector stopped")
121 | }
122 | }
123 | }()
124 |
125 | // Load service's version
126 | err = loadVersion()
127 | if err != nil {
128 | log.Fatal().Msg("Can't load version: " + err.Error())
129 | }
130 |
131 | /*
132 | * Start an HTTPS server
133 | */
134 | http.HandleFunc("/api", apiHandler)
135 |
136 | log.Info().Msgf("Graphoscope v%s. Starting the service listening on %s:%s", version, config.Server.Host, config.Server.Port)
137 | server := setupTLSserver()
138 |
139 | err = server.ListenAndServeTLS(config.Server.CertFile, config.Server.KeyFile)
140 | if err != nil {
141 | log.Fatal().Msg("Can't ListenAndServeTLS: " + err.Error())
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/notifications.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | /*
8 | * Structure of one Web UI notification
9 | */
10 | type Notification struct {
11 | // Type of notification.
12 | // Currently possible values: "err", "info"
13 | Type string `bson:"type" json:"type"`
14 |
15 | // Message text
16 | Message string `bson:"message" json:"message"`
17 |
18 | // Timestamp of the creation time
19 | Ts string `bson:"ts" json:"ts"`
20 | }
21 |
22 | /*
23 | * Add new notification to the user.
24 | *
25 | * If user is online - notification will be received in a browser,
26 | * If offline - it will be stored in a database.
27 | *
28 | * Receives its type and message text
29 | */
30 | func (a *Account) addNotification(typ, message string) {
31 | n := &Notification{
32 | Type: typ,
33 | Message: message,
34 | Ts: time.Now().Format("2 Jan 2006 15:04:05"),
35 | }
36 |
37 | // Check whether user is online first
38 | if a.Session != nil {
39 | a.send("notification", message, typ)
40 | return
41 | }
42 |
43 | // Store the notification in a database otherwise
44 | a.Notifications = append(a.Notifications, n)
45 |
46 | // Leave the last 5 notifications only
47 | if len(a.Notifications) > 5 {
48 | a.Notifications = a.Notifications[len(a.Notifications)-5:]
49 | }
50 |
51 | err := a.update("notifications", a.Notifications)
52 | if err != nil {
53 | log.Error().
54 | Str("username", a.Username).
55 | Msg("Can't add a notification to the user: " + err.Error())
56 | }
57 |
58 | log.Info().
59 | Str("ip", a.Session.IP).
60 | Str("username", a.Username).
61 | Msg("Notification added")
62 | }
63 |
64 | /*
65 | * Handle 'notifications' websocket command
66 | * to clean user's notifications
67 | */
68 | func (a *Account) notificationsHandler() {
69 |
70 | a.Notifications = []*Notification{}
71 |
72 | err := a.update("notifications", a.Notifications)
73 | if err != nil {
74 | log.Error().
75 | Str("ip", a.Session.IP).
76 | Str("username", a.Username).
77 | Msg("Can't clean notifications: " + err.Error())
78 |
79 | a.send("error", err.Error(), "Can't clean notifications!")
80 | return
81 | }
82 |
83 | log.Info().
84 | Str("ip", a.Session.IP).
85 | Str("username", a.Username).
86 | Msg("Notifications cleaned")
87 | }
88 |
--------------------------------------------------------------------------------
/pdk/plugin.go:
--------------------------------------------------------------------------------
1 | package pdk
2 |
3 | import (
4 | "github.com/blastrain/vitess-sqlparser/sqlparser"
5 | )
6 |
7 | /*
8 | * Plugin interface to be implemented by the data source plugins
9 | */
10 | type SourcePlugin interface {
11 | // Return data source instance configuration
12 | Conf() *Source
13 |
14 | // Set specific parameters for the data source instance,
15 | // establish connection, etc.
16 | Setup(*Source, int) error
17 |
18 | // Get a list of all known data source's fields
19 | // for the Web GUI autocomplete
20 | Fields() ([]string, error)
21 |
22 | // Execute the given query.
23 | // Returns results, statistics, debug info & error
24 | Search(*sqlparser.Select) ([]map[string]interface{}, map[string]interface{}, map[string]interface{}, error)
25 |
26 | // Stop the collector when the core service stops,
27 | // gracefully disconnect from the data source if needed
28 | Stop() error
29 | }
30 |
31 | /*
32 | * Plugin interface to be implemented by the processor plugins
33 | */
34 | type ProcessorPlugin interface {
35 | // Return instance configuration
36 | Conf() *Processor
37 |
38 | // Set specific parameters for the instance,
39 | // establish connection, etc.
40 | Setup(*Processor) error
41 |
42 | // Process data source's received data in a background
43 | Process([]map[string]interface{}) ([]map[string]interface{}, error)
44 |
45 | // Stop the processor when the core service stops,
46 | // gracefully disconnect if needed
47 | Stop() error
48 | }
49 |
--------------------------------------------------------------------------------
/pdk/processor.go:
--------------------------------------------------------------------------------
1 | /*
2 | * Data processing definition.
3 | * For YAML files in "../processor" by default.
4 | *
5 | * Check "../definitions/processors/processor.yaml.example" for the fields description
6 | */
7 |
8 | package pdk
9 |
10 | import (
11 | "time"
12 | )
13 |
14 | type Processor struct {
15 | Name string `yaml:"name"`
16 | Plugin string `yaml:"plugin"`
17 | Timeout time.Duration `yaml:"timeout"`
18 | Data map[string]interface{} `yaml:"data"`
19 | }
20 |
--------------------------------------------------------------------------------
/pdk/source.go:
--------------------------------------------------------------------------------
1 | /*
2 | * Data source definition.
3 | * For YAML files in "../sources" by default.
4 | *
5 | * Check "../sources/source.yaml.example" for the fields description
6 | */
7 |
8 | package pdk
9 |
10 | import (
11 | "regexp"
12 | "time"
13 | )
14 |
15 | type Source struct {
16 | Name string `yaml:"name"`
17 | Label string `yaml:"label"`
18 | Icon string `yaml:"icon"`
19 | Plugin string `yaml:"plugin"`
20 | InGlobal bool `yaml:"inGlobal"`
21 | IncludeDatetime bool `yaml:"includeDatetime"`
22 | SupportsSQL bool `yaml:"supportsSQL"`
23 | Timeout time.Duration `yaml:"timeout"`
24 | Access map[string]string `yaml:"access"`
25 | QueryFields []string `yaml:"queryFields"`
26 | IncludeFields []string `yaml:"includeFields"`
27 | StatsFields []string `yaml:"statsFields"`
28 | ReplaceFields map[string]string `yaml:"replaceFields"`
29 | Relations []*Relation `yaml:"relations"`
30 | }
31 |
32 | type Relation struct {
33 | From *Node `yaml:"from"`
34 | To *Node `yaml:"to"`
35 | Edge *struct {
36 | Label string `yaml:"label"`
37 | Attributes []string `yaml:"attributes"`
38 | } `yaml:"edge"`
39 | }
40 |
41 | type Node struct {
42 | ID string `yaml:"id"`
43 | Group string `yaml:"group"`
44 | Search string `yaml:"search"`
45 | Attributes []string `yaml:"attributes"`
46 | VarTypes []*struct {
47 | Regex string `yaml:"regex"`
48 | RegexCompiled *regexp.Regexp `yaml:"-"`
49 | Group string `yaml:"group"`
50 | Search string `yaml:"search"`
51 | Label string `yaml:"label"`
52 | } `yaml:"varTypes"`
53 | }
54 |
--------------------------------------------------------------------------------
/pdk/stats.go:
--------------------------------------------------------------------------------
1 | package pdk
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 |
7 | "github.com/umpc/go-sortedmap"
8 | )
9 |
10 | /*
11 | * Structure to contain statistics data
12 | * if some data source has too many entries to return
13 | * (above the preconfigured limit)
14 | */
15 | type Stats struct {
16 | Fields map[string]*sortedmap.SortedMap
17 | mx sync.Mutex
18 | }
19 |
20 | func NewStats() *Stats {
21 | return &Stats{
22 | Fields: make(map[string]*sortedmap.SortedMap),
23 | }
24 | }
25 |
26 | /*
27 | * Update statistics of the received entries from some data source.
28 | * When the amount of returned entries becomes too large
29 | * users will receive the statistics info instead of the graph relations data.
30 | *
31 | * Receives:
32 | * entry - single entry from a data source
33 | * key - statistics chart field to update,
34 | * one entry increases the value by 1
35 | */
36 | func (s *Stats) Update(entry map[string]interface{}, key string) {
37 | // Skip if value is missing
38 | value := entry[key]
39 | if value == nil || fmt.Sprint(value) == "" {
40 | return
41 | }
42 |
43 | s.mx.Lock()
44 |
45 | if val, ok := s.Fields[key].Get(value); ok {
46 | s.Fields[key].Replace(value, val.(int)+1)
47 | } else {
48 | s.Fields[key].Insert(value, 1)
49 | }
50 |
51 | s.mx.Unlock()
52 | }
53 |
54 | /*
55 | * Convert sorted-map object to the native map,
56 | * converted to the JSON later,
57 | * so the Web GUI can draw interactive charts.
58 | *
59 | * Receives a data source name
60 | */
61 | func (s *Stats) ToJSON(source string) (map[string]interface{}, error) {
62 |
63 | // Map to store Top 10 entries
64 | json := make(map[string]interface{})
65 |
66 | // Identifier of the source data belongs to
67 | json["source"] = source
68 |
69 | for k, v := range s.Fields {
70 | i := 1
71 |
72 | iterCh, err := v.IterCh()
73 | if err != nil && len(v.Keys()) != 0 {
74 | return nil, err
75 |
76 | } else if len(v.Keys()) != 0 {
77 | defer iterCh.Close()
78 |
79 | group := make(map[string]int)
80 |
81 | for rec := range iterCh.Records() {
82 | //fmt.Printf("%+v\n", rec)
83 |
84 | group[fmt.Sprint(rec.Key)] = rec.Val.(int)
85 |
86 | // We want Top 10 here and started from i == 1
87 | if i > 9 {
88 | break
89 | }
90 |
91 | i++
92 | }
93 |
94 | json[k] = group
95 | }
96 | }
97 |
98 | return json, nil
99 | }
100 |
--------------------------------------------------------------------------------
/plugins/src/abuseipdb/README.md:
--------------------------------------------------------------------------------
1 | # AbuseIPDB plugin
2 |
3 | AbuseIPDB connector sends IP address in a GET request to the `abuseipdb.com` API and expects a list of reports back.
4 | More info: https://docs.abuseipdb.com/#check-endpoint
5 |
6 |
7 | Simple request to the API:
8 | ```
9 | curl -G https://api.abuseipdb.com/api/v2/check \
10 | --data-urlencode "ipAddress=8.8.8.8" \
11 | -d maxAgeInDays=90 \
12 | -d verbose \
13 | -H "Key: YOUR_OWN_API_KEY" \
14 | -H "Accept: application/json"
15 | ```
16 | where `YOUR_OWN_API_KEY` is your personal/unique API key.
17 |
18 |
19 | `curl` to test plugin:
20 | ```sh
21 | curl 'https://localhost:443/api?uuid=auth-key&sql=FROM+abuseipdb+WHERE+ip=%278.8.8.8%27'
22 | ```
23 |
24 | Compile with:
25 | ```sh
26 | go build -buildmode=plugin -ldflags="-w" -o abuseipdb.so ./*.go
27 | ```
28 |
29 | # Limitations
30 |
31 | Does not support complex SQL queries and datetime range selection.
32 |
33 |
34 | # Access details
35 |
36 | Source YAML definition's `access` fields:
37 | - **url**: HTTPS access point, `https://api.abuseipdb.com/api/v2/check` at the moment
38 | - **maxAgeInDays**: how far back in time we go to fetch reports, max 365
39 | - **key**: unique API key
40 |
41 |
42 | # Definition file example
43 |
44 | Replace API key with your own:
45 | ```yaml
46 | name: abuseipdb
47 | label: AbuseIPDB
48 | icon: clipboard list
49 |
50 | plugin: abuseipdb
51 | inGlobal: true
52 | includeDatetime: false
53 | supportsSQL: false
54 |
55 | access:
56 | url: https://api.abuseipdb.com/api/v2/check
57 | maxAgeInDays: 180
58 | key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
59 |
60 | queryFields:
61 | - ip
62 |
63 | replaceFields:
64 | ip: ipAddress
65 |
66 |
67 | relations:
68 | -
69 | from:
70 | id: domain
71 | group: domain
72 | search: domain
73 |
74 | to:
75 | id: ipAddress
76 | group: ip
77 | search: ip
78 | attributes: [ "countryCode", "countryName", "hostnames", "isPublic", "isWhitelisted", "isp", "usageType", "totalReports", "lastReportedAt" ]
79 |
80 | ```
--------------------------------------------------------------------------------
/plugins/src/abuseipdb/abuseipdb_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted [][2]string
21 | }{
22 | {`SELECT * WHERE ipAddress='10.10.10.10' LIMIT 5,1`, [][2]string{
23 | [2]string{"ipAddress", "10.10.10.10"},
24 | }},
25 | {`SELECT * WHERE ipAddress='8.8.8.8' or domain='google.com'`, [][2]string{
26 | [2]string{"ipAddress", "8.8.8.8"},
27 | }},
28 | {`SELECT * WHERE ip='10.10.10.10' LIMIT 5,1`, [][2]string{}},
29 | }
30 |
31 | for _, table := range tables {
32 | // Executed by the main service
33 | ast, err := sqlparser.Parse(table.sql)
34 | if err != nil {
35 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
36 | continue
37 | }
38 |
39 | stmt, ok := ast.(*sqlparser.Select)
40 | if !ok {
41 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
42 | continue
43 | }
44 |
45 | // Executed by the plugin
46 | result, err := c.convert(stmt)
47 | if err != nil {
48 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
49 | continue
50 | }
51 |
52 | if !equal(result, table.converted) {
53 | t.Errorf("Invalid conversion of '%s': %v, expected: %v", table.sql, result, table.converted)
54 | }
55 | }
56 | }
57 |
58 | /*
59 | * Check whether a and b slices contain the same elements
60 | */
61 | func equal(a, b [][2]string) bool {
62 | if len(a) != len(b) {
63 | return false
64 | }
65 | for i, v := range a {
66 | if v != b[i] {
67 | return false
68 | }
69 | }
70 | return true
71 | }
72 |
--------------------------------------------------------------------------------
/plugins/src/abuseipdb/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to the field/value list convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "github.com/blastrain/vitess-sqlparser/sqlparser"
9 | )
10 |
11 | /*
12 | * Convert SQL query to the list of [field,value]
13 | */
14 | func (p *plugin) convert(sel *sqlparser.Select) ([][2]string, error) {
15 |
16 | // Handle WHERE.
17 | // Top level node pass in an empty interface
18 | // to tell the children this is root.
19 | // Is there any better way?
20 | var rootParent sqlparser.Expr
21 |
22 | // List of requested fields & values
23 | fields, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
24 | if err != nil {
25 | return nil, err
26 | }
27 |
28 | i := 0
29 | for _, field := range fields {
30 | if field[0] != "ipAddress" {
31 | fields = append(fields[:i], fields[i+1:]...)
32 | i -= 1
33 | }
34 |
35 | i += 1
36 | }
37 |
38 | return fields, nil
39 | }
40 |
--------------------------------------------------------------------------------
/plugins/src/abuseipdb/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Export symbols
9 | */
10 | var (
11 | Name = "abuseipdb"
12 | Version = "1.0.1"
13 | Plugin plugin
14 | )
15 |
16 | /*
17 | * Structure to be imported by the core as a plugin
18 | */
19 | type plugin struct {
20 |
21 | // Inherit default configuration fields
22 | source *pdk.Source
23 |
24 | // Custom fields
25 | url string
26 | maxAgeInDays int
27 | key string
28 | limit int
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/src/circl_passive_ssl/README.md:
--------------------------------------------------------------------------------
1 | # CIRCL Passive SSL plugin
2 |
3 | Data source: https://www.circl.lu/services/passive-ssl/
4 |
5 | Connector sends a GET request to the `https://www.circl.lu/v2pssl/` and expects a JSON back.
6 | To the preconfigured URL `field/value` will be attached as query.
7 |
8 | `curl` to test:
9 | ```sh
10 | curl 'https://localhost:443/api?uuid=auth-key&sql=FROM+service+WHERE+ip=%278.8.8.8%27'
11 | ```
12 |
13 | Compile with:
14 | ```sh
15 | go build -buildmode=plugin -ldflags="-w" -o circl_passive_ssl.so ./*.go
16 | ```
17 |
18 | # Limitations
19 |
20 | Does not support complex SQL queries and datetime range selection.
21 |
22 |
23 | # Access details
24 |
25 | Source YAML definition's `access` fields:
26 | - **url**: REST API access point, `https://www.circl.lu/v2pssl/`
27 | - **username**: Username
28 | - **password**: User's password
29 |
30 |
31 | # YAML definition example
32 |
33 | ```yaml
34 | name: circl_passive_ssl
35 | label: CIRCL Passive SSL
36 | icon: retweet
37 |
38 | plugin: circl_passive_ssl
39 | inGlobal: false
40 | includeDatetime: false
41 | supportsSQL: false
42 |
43 | access:
44 | url: https://www.circl.lu/v2pssl
45 | username: user
46 | password: password_
47 |
48 | queryFields:
49 | - ip
50 | - network
51 | - sha1
52 |
53 | statsFields:
54 | - ip
55 | - sha1
56 | - subject
57 |
58 |
59 | relations:
60 | -
61 | from:
62 | id: sha1
63 | group: sha1
64 | search: sha1
65 | attributes: ["subject"]
66 |
67 | to:
68 | id: ip
69 | group: ip
70 | search: ip
71 | ```
72 |
--------------------------------------------------------------------------------
/plugins/src/circl_passive_ssl/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to the field/value list convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "github.com/blastrain/vitess-sqlparser/sqlparser"
9 | )
10 |
11 | /*
12 | * Convert SQL query to the list of [field,value]
13 | */
14 | func (p *plugin) convert(sel *sqlparser.Select) ([2]string, error) {
15 |
16 | // Handle WHERE.
17 | // Top level node pass in an empty interface
18 | // to tell the children this is root.
19 | // Is there any better way?
20 | var rootParent sqlparser.Expr
21 |
22 | // List of requested fields & values
23 | fields, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
24 | if err != nil {
25 | return [2]string{}, err
26 | }
27 |
28 | return fields, nil
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/src/circl_passive_ssl/passive_ssl_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted [2]string
21 | }{
22 | {`SELECT * WHERE ip='10.10.10.10' LIMIT 5,1`, [2]string{"ip", "10.10.10.10"}},
23 | {`select * where sha1='00000'`, [2]string{"sha1", "00000"}},
24 | }
25 |
26 | for _, table := range tables {
27 | // Executed by the main service
28 | ast, err := sqlparser.Parse(table.sql)
29 | if err != nil {
30 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
31 | continue
32 | }
33 |
34 | stmt, ok := ast.(*sqlparser.Select)
35 | if !ok {
36 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
37 | continue
38 | }
39 |
40 | // Executed by the plugin
41 | result, err := c.convert(stmt)
42 | if err != nil {
43 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
44 | continue
45 | }
46 |
47 | if !equal(result, table.converted) {
48 | t.Errorf("Invalid conversion of '%s': %v, expected: %v", table.sql, result, table.converted)
49 | }
50 | }
51 | }
52 |
53 | /*
54 | * Check whether a and b slices contain the same elements
55 | */
56 | func equal(a, b [2]string) bool {
57 | for i, v := range a {
58 | if v != b[i] {
59 | return false
60 | }
61 | }
62 | return true
63 | }
64 |
--------------------------------------------------------------------------------
/plugins/src/circl_passive_ssl/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Export symbols
9 | */
10 | var (
11 | Name = "circl_passive_ssl"
12 | Version = "1.0.1"
13 | Plugin plugin
14 | )
15 |
16 | /*
17 | * Structure to be imported by the core as a plugin
18 | */
19 | type plugin struct {
20 |
21 | // Inherit default configuration fields
22 | source *pdk.Source
23 |
24 | // Custom fields
25 | url string
26 | username string
27 | password string
28 | limit int
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/src/elasticsearch.v7/README.md:
--------------------------------------------------------------------------------
1 | # Elasticsearch plugin
2 |
3 | Plugin to query Elasticsearch (https://www.elastic.co/elasticsearch) v7 as a data source.
4 |
5 | SQl convertor's base: https://github.com/blastrain/vitess-sqlparser/tree/develop/sqlparser
6 |
7 |
8 | Compile with:
9 | ```sh
10 | go build -buildmode=plugin -ldflags="-w" -o elasticsearch.v7.so ./*.go
11 | ```
12 |
13 | # Access details
14 |
15 | Source YAML definition's `access` fields:
16 | - **url**: HTTP access point, for example - `http://localhost:9200`
17 | - **username**: username for the basic auth
18 | - **password**: password for the basic auth
19 | - **key**: authorization key
20 | - **indices**: comma separated indices patterns to query, for example - `apps-*`
21 | - **ca**: CA certificate path
22 |
23 | Only `username/password` or `key` can be used at once.
24 |
25 |
26 | ## Limitations
27 |
28 | - Go package supports specific Elasticsearch major version only,
29 | so version number is included in a plugin's name
30 |
--------------------------------------------------------------------------------
/plugins/src/elasticsearch.v7/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to Elasticsearch query convertor
3 | * Based on: https://github.com/blastrain/vitess-sqlparser/tree/develop/sqlparser
4 | */
5 |
6 | package main
7 |
8 | import (
9 | "errors"
10 | "fmt"
11 | "strings"
12 |
13 | "github.com/blastrain/vitess-sqlparser/sqlparser"
14 | )
15 |
16 | /*
17 | * Convert SQL query to the Elasticsearch JSON query
18 | */
19 | func (p *plugin) convert(sel *sqlparser.Select, fields []string) (string, error) {
20 |
21 | // Handle WHERE.
22 | // Top level node pass in an empty interface
23 | // to tell the children this is root.
24 | // Is there any better way?
25 | var rootParent sqlparser.Expr
26 |
27 | queryMapStr, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
28 | if err != nil {
29 | return "", err
30 | }
31 |
32 | // Handle GROUP BY
33 | if len(sel.GroupBy) > 0 || checkNeedAgg(sel.SelectExprs) {
34 | return "", errors.New("'GROUP BY' & aggregation are not supported")
35 | }
36 |
37 | resultMap := make(map[string]interface{})
38 | resultMap["query"] = queryMapStr
39 |
40 | // Handle ORDER BY
41 | orderByArr := []string{}
42 | for _, orderByExpr := range sel.OrderBy {
43 | orderByStr := fmt.Sprintf(`{"%v": "%v"}`, strings.Replace(sqlparser.String(orderByExpr.Expr), "`", "", -1), orderByExpr.Direction)
44 | orderByArr = append(orderByArr, orderByStr)
45 | }
46 |
47 | if len(orderByArr) > 0 {
48 | resultMap["sort"] = fmt.Sprintf("[%v]", strings.Join(orderByArr, ", "))
49 | }
50 |
51 | // Handle LIMIT
52 | if sel.Limit != nil {
53 | resultMap["from"] = sqlparser.String(sel.Limit.Offset)
54 | resultMap["size"] = sqlparser.String(sel.Limit.Rowcount)
55 | }
56 |
57 | if len(fields) != 0 {
58 | resultMap["_source"] = "[\"" + strings.Join(fields, "\",\"") + "\"]"
59 | }
60 |
61 | // Fields of the JSON to return.
62 | // Keep the traversal in order, avoid unpredicted JSON
63 | keySlice := []string{"query", "from", "size", "sort", "_source"}
64 | resultArr := []string{}
65 |
66 | for _, mapKey := range keySlice {
67 | if val, ok := resultMap[mapKey]; ok {
68 | resultArr = append(resultArr, fmt.Sprintf(`"%v" : %v`, mapKey, val))
69 | }
70 | }
71 |
72 | dsl := "{" + strings.Join(resultArr, ", ") + "}"
73 | //fmt.Println(dsl)
74 |
75 | // Return a JSON formatted query
76 | return dsl, nil
77 | }
78 |
--------------------------------------------------------------------------------
/plugins/src/elasticsearch.v7/elasticsearch_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted string
21 | }{
22 | {`SELECT * WHERE ip='10.10.10.10'`, `{"query" : {"bool" : {"must" : [{"match_phrase" : {"ip" : "10.10.10.10"}}]}}}`},
23 | {`SELECT * WHERE ip='10.10.10.10' LIMIT 5,10`, `{"query" : {"bool" : {"must" : [{"match_phrase" : {"ip" : "10.10.10.10"}}]}}, "from" : 5, "size" : 10}`},
24 | {`SELECT * WHERE size>100 ORDER BY name LIMIT 0,1`, `{"query" : {"bool" : {"must" : [{"range" : {"size" : {"gt" : 100}}}]}}, "from" : 0, "size" : 1, "sort" : [{"name": "asc"}]}`},
25 | {`SELECT * WHERE size=10 ORDER BY name DESC LIMIT 0,1`, `{"query" : {"bool" : {"must" : [{"match_phrase" : {"size" : 10}}]}}, "from" : 0, "size" : 1, "sort" : [{"name": "desc"}]}`},
26 | {`SELECT * WHERE size>=100`, `{"query" : {"bool" : {"must" : [{"range" : {"size" : {"from" : 100}}}]}}}`},
27 | {`SELECT * WHERE name LIKE 's%'`, `{"query" : {"bool" : {"must" : [{"query_string": { "default_field": "name.keyword", "query": "s*" }}]}}}`},
28 | {`SELECT * WHERE name NOT LIKE 's%'`, `{"query" : {"bool" : {"must" : [{"bool" : {"must_not" : {"query_string": { "default_field": "name.keyword", "query": "s*" }}}}]}}}`},
29 | {`SELECT * WHERE size BETWEEN 100 AND 300`, `{"query" : {"bool" : {"must" : [{"range" : {"size" : {"from" : 100, "to" : 300}}}]}}}`},
30 | {`SELECT * WHERE size NOT BETWEEN 1 AND 10`, `{"query" : {"bool" : {"must" : [{"bool" : {"must_not" : {"range" : {"size" : {"from" : 1, "to" : 10}}}}}]}}}`},
31 | {`SELECT * WHERE size IN (100,300)`, `{"query" : {"bool" : {"must" : [{"terms" : {"size" : [100, 300]}}]}}}`},
32 | {`SELECT * WHERE size NOT IN (100,300)`, `{"query" : {"bool" : {"must" : [{"bool" : {"must_not" : {"terms" : {"size" : [100, 300]}}}}]}}}`},
33 | {`select * where name='sarah' and age!=40 and (country='LV' or country='AU') limit 0,1`, `{"query" : {"bool" : {"must" : [{"match_phrase" : {"name" : "sarah"}}, {"bool" : {"must_not" : [{"match_phrase" : {"age" : 40}}]}}, {"bool" : {"should" : [{"match_phrase" : {"country" : "LV"}}, {"match_phrase" : {"country" : "AU"}}]}}]}}, "from" : 0, "size" : 1}`},
34 | }
35 |
36 | for _, table := range tables {
37 | // Executed by the main service
38 | ast, err := sqlparser.Parse(table.sql)
39 | if err != nil {
40 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
41 | continue
42 | }
43 |
44 | stmt, ok := ast.(*sqlparser.Select)
45 | if !ok {
46 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
47 | continue
48 | }
49 |
50 | // Executed by the plugin
51 | result, err := c.convert(stmt, nil)
52 | if err != nil {
53 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
54 | continue
55 | }
56 |
57 | if result != table.converted {
58 | t.Errorf("Invalid conversion of '%s': %s, expected: %s", table.sql, result, table.converted)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/plugins/src/elasticsearch.v7/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | "github.com/elastic/go-elasticsearch/v7"
6 | )
7 |
8 | /*
9 | * Export symbols
10 | */
11 | var (
12 | Name = "elasticsearch.v7"
13 | Version = "1.0.9"
14 | Plugin plugin
15 | )
16 |
17 | /*
18 | * Structure to be imported by the core as a plugin
19 | */
20 | type plugin struct {
21 |
22 | // Inherit default configuration fields
23 | source *pdk.Source
24 |
25 | // Custom fields
26 | client *elasticsearch.Client
27 | index string
28 | limit int
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/src/elasticsearch.v8/README.md:
--------------------------------------------------------------------------------
1 | # Elasticsearch plugin
2 |
3 | Plugin to query Elasticsearch (https://www.elastic.co/elasticsearch) v8 as a data source.
4 |
5 | SQl convertor's base: https://github.com/blastrain/vitess-sqlparser/tree/develop/sqlparser
6 |
7 |
8 | Compile with:
9 | ```sh
10 | go build -buildmode=plugin -ldflags="-w" -o elasticsearch.v8.so ./*.go
11 | ```
12 |
13 | # Access details
14 |
15 | Source YAML definition's `access` fields:
16 | - **url**: HTTP access point, for example - `http://localhost:9200`
17 | - **username**: username for the basic auth
18 | - **password**: password for the basic auth
19 | - **key**: authorization key
20 | - **indices**: comma separated indices patterns to query, for example - `apps-*`
21 | - **ca**: CA certificate path
22 |
23 | Only `username/password` or `key` can be used at once.
24 |
25 |
26 | ## Limitations
27 |
28 | - Go package supports specific Elasticsearch major version only,
29 | so version number is included in a plugin's name
30 |
--------------------------------------------------------------------------------
/plugins/src/elasticsearch.v8/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to Elasticsearch query convertor
3 | * Based on: https://github.com/blastrain/vitess-sqlparser/tree/develop/sqlparser
4 | */
5 |
6 | package main
7 |
8 | import (
9 | "errors"
10 | "fmt"
11 | "strings"
12 |
13 | "github.com/blastrain/vitess-sqlparser/sqlparser"
14 | )
15 |
16 | /*
17 | * Convert SQL query to the Elasticsearch JSON query
18 | */
19 | func (p *plugin) convert(sel *sqlparser.Select, fields []string) (string, error) {
20 |
21 | // Handle WHERE.
22 | // Top level node pass in an empty interface
23 | // to tell the children this is root.
24 | // Is there any better way?
25 | var rootParent sqlparser.Expr
26 |
27 | queryMapStr, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
28 | if err != nil {
29 | return "", err
30 | }
31 |
32 | // Handle GROUP BY
33 | if len(sel.GroupBy) > 0 || checkNeedAgg(sel.SelectExprs) {
34 | return "", errors.New("'GROUP BY' & aggregation are not supported")
35 | }
36 |
37 | resultMap := make(map[string]interface{})
38 | resultMap["query"] = queryMapStr
39 |
40 | // Handle ORDER BY
41 | orderByArr := []string{}
42 | for _, orderByExpr := range sel.OrderBy {
43 | orderByStr := fmt.Sprintf(`{"%v": "%v"}`, strings.Replace(sqlparser.String(orderByExpr.Expr), "`", "", -1), orderByExpr.Direction)
44 | orderByArr = append(orderByArr, orderByStr)
45 | }
46 |
47 | if len(orderByArr) > 0 {
48 | resultMap["sort"] = fmt.Sprintf("[%v]", strings.Join(orderByArr, ", "))
49 | }
50 |
51 | // Handle LIMIT
52 | if sel.Limit != nil {
53 | resultMap["from"] = sqlparser.String(sel.Limit.Offset)
54 | resultMap["size"] = sqlparser.String(sel.Limit.Rowcount)
55 | }
56 |
57 | if len(fields) != 0 {
58 | resultMap["_source"] = "[\"" + strings.Join(fields, "\",\"") + "\"]"
59 | }
60 |
61 | // Fields of the JSON to return.
62 | // Keep the traversal in order, avoid unpredicted JSON
63 | keySlice := []string{"query", "from", "size", "sort", "_source"}
64 | resultArr := []string{}
65 |
66 | for _, mapKey := range keySlice {
67 | if val, ok := resultMap[mapKey]; ok {
68 | resultArr = append(resultArr, fmt.Sprintf(`"%v" : %v`, mapKey, val))
69 | }
70 | }
71 |
72 | dsl := "{" + strings.Join(resultArr, ", ") + "}"
73 | //fmt.Println(dsl)
74 |
75 | // Return a JSON formatted query
76 | return dsl, nil
77 | }
78 |
--------------------------------------------------------------------------------
/plugins/src/elasticsearch.v8/elasticsearch_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted string
21 | }{
22 | {`SELECT * WHERE ip='10.10.10.10'`, `{"query" : {"bool" : {"must" : [{"match_phrase" : {"ip" : "10.10.10.10"}}]}}}`},
23 | {`SELECT * WHERE ip='10.10.10.10' LIMIT 5,10`, `{"query" : {"bool" : {"must" : [{"match_phrase" : {"ip" : "10.10.10.10"}}]}}, "from" : 5, "size" : 10}`},
24 | {`SELECT * WHERE size>100 ORDER BY name LIMIT 0,1`, `{"query" : {"bool" : {"must" : [{"range" : {"size" : {"gt" : 100}}}]}}, "from" : 0, "size" : 1, "sort" : [{"name": "asc"}]}`},
25 | {`SELECT * WHERE size=10 ORDER BY name DESC LIMIT 0,1`, `{"query" : {"bool" : {"must" : [{"match_phrase" : {"size" : 10}}]}}, "from" : 0, "size" : 1, "sort" : [{"name": "desc"}]}`},
26 | {`SELECT * WHERE size>=100`, `{"query" : {"bool" : {"must" : [{"range" : {"size" : {"from" : 100}}}]}}}`},
27 | {`SELECT * WHERE name LIKE 's%'`, `{"query" : {"bool" : {"must" : [{"query_string": { "default_field": "name.keyword", "query": "s*" }}]}}}`},
28 | {`SELECT * WHERE name NOT LIKE 's%'`, `{"query" : {"bool" : {"must" : [{"bool" : {"must_not" : {"query_string": { "default_field": "name.keyword", "query": "s*" }}}}]}}}`},
29 | {`SELECT * WHERE size BETWEEN 100 AND 300`, `{"query" : {"bool" : {"must" : [{"range" : {"size" : {"from" : 100, "to" : 300}}}]}}}`},
30 | {`SELECT * WHERE size NOT BETWEEN 1 AND 10`, `{"query" : {"bool" : {"must" : [{"bool" : {"must_not" : {"range" : {"size" : {"from" : 1, "to" : 10}}}}}]}}}`},
31 | {`SELECT * WHERE size IN (100,300)`, `{"query" : {"bool" : {"must" : [{"terms" : {"size" : [100, 300]}}]}}}`},
32 | {`SELECT * WHERE size NOT IN (100,300)`, `{"query" : {"bool" : {"must" : [{"bool" : {"must_not" : {"terms" : {"size" : [100, 300]}}}}]}}}`},
33 | {`select * where name='sarah' and age!=40 and (country='LV' or country='AU') limit 0,1`, `{"query" : {"bool" : {"must" : [{"match_phrase" : {"name" : "sarah"}}, {"bool" : {"must_not" : [{"match_phrase" : {"age" : 40}}]}}, {"bool" : {"should" : [{"match_phrase" : {"country" : "LV"}}, {"match_phrase" : {"country" : "AU"}}]}}]}}, "from" : 0, "size" : 1}`},
34 | }
35 |
36 | for _, table := range tables {
37 | // Executed by the main service
38 | ast, err := sqlparser.Parse(table.sql)
39 | if err != nil {
40 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
41 | continue
42 | }
43 |
44 | stmt, ok := ast.(*sqlparser.Select)
45 | if !ok {
46 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
47 | continue
48 | }
49 |
50 | // Executed by the plugin
51 | result, err := c.convert(stmt, nil)
52 | if err != nil {
53 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
54 | continue
55 | }
56 |
57 | if result != table.converted {
58 | t.Errorf("Invalid conversion of '%s': %s, expected: %s", table.sql, result, table.converted)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/plugins/src/elasticsearch.v8/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | "github.com/elastic/go-elasticsearch/v8"
6 | )
7 |
8 | /*
9 | * Export symbols
10 | */
11 | var (
12 | Name = "elasticsearch.v8"
13 | Version = "1.0.2"
14 | Plugin plugin
15 | )
16 |
17 | /*
18 | * Structure to be imported by the core as a plugin
19 | */
20 | type plugin struct {
21 |
22 | // Inherit default configuration fields
23 | source *pdk.Source
24 |
25 | // Custom fields
26 | client *elasticsearch.Client
27 | index string
28 | limit int
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/src/file/csv/README.md:
--------------------------------------------------------------------------------
1 | # CSV file plugin
2 |
3 | Plugin to query CSV file as a data source.
4 |
5 | SQL doesn't allow to query missing columns, like Elasticsearch does.
6 | An error `field X does not exist` will be received. That means you must be very
7 | careful with designing a data source and creating a YAML config file to be able
8 | to combine it with data source types other than SQL.
9 |
10 | The easiest solution is to exclude data source from the `global` namespace
11 | and query it independently, to make sure all columns exist.
12 |
13 | `curl` to test:
14 | ```sh
15 | curl 'https://localhost:443/api?uuid=auth-key&sql=FROM+csvfile+WHERE+ip=%278.8.8.8%27'
16 | ```
17 |
18 | Compile with:
19 | ```sh
20 | go build -buildmode=plugin -ldflags="-w" -o file-csv.so ./*.go
21 | ```
22 |
23 | # Access details
24 |
25 | Source YAML definition's `access` fields:
26 | - **path**: CSV file to use, for example - `/data/test.csv`
27 |
--------------------------------------------------------------------------------
/plugins/src/file/csv/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to CSV query convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "github.com/blastrain/vitess-sqlparser/sqlparser"
9 | )
10 |
11 | /*
12 | * Convert SQL statement to the CSV query
13 | */
14 | func (p *plugin) convert(sel *sqlparser.Select) (string, error) {
15 |
16 | // Handle WHERE
17 | query := sqlparser.String(sel.Where.Expr)
18 |
19 | // Handle GROUP BY
20 | if len(sel.GroupBy) > 0 {
21 | query += sqlparser.String(sel.GroupBy)
22 | }
23 |
24 | // Handle ORDER BY
25 | if sel.OrderBy != nil {
26 | query += sqlparser.String(sel.OrderBy)
27 | }
28 |
29 | // Handle LIMIT
30 | if sel.Limit != nil {
31 | // Handle rowcount
32 | query += " LIMIT " + sqlparser.String(sel.Limit.Rowcount)
33 | // Handle offset
34 | if sel.Limit.Offset != nil {
35 | query += " OFFSET " + sqlparser.String(sel.Limit.Offset)
36 | } else {
37 | query += " OFFSET 0"
38 | }
39 | }
40 |
41 | return query, nil
42 | }
43 |
--------------------------------------------------------------------------------
/plugins/src/file/csv/csv_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted string
21 | }{
22 | {`SELECT * WHERE ip='10.10.10.10'`, `ip = '10.10.10.10'`},
23 | {`SELECT * WHERE ip='10.10.10.10' LIMIT 5,10`, `ip = '10.10.10.10' LIMIT 10 OFFSET 5`},
24 | {`SELECT * WHERE size>100 ORDER BY name LIMIT 0,1`, `size > 100 order by name asc LIMIT 1 OFFSET 0`},
25 | {`SELECT * WHERE size=10 ORDER BY name DESC LIMIT 0,1`, `size = 10 order by name desc LIMIT 1 OFFSET 0`},
26 | {`SELECT * WHERE size>=100`, `size >= 100`},
27 | {`SELECT * WHERE name LIKE 's%'`, `name like 's%'`},
28 | {`SELECT * WHERE name NOT LIKE 's%'`, `name not like 's%'`},
29 | {`SELECT * WHERE size BETWEEN 100 AND 300`, `size between 100 and 300`},
30 | {`SELECT * WHERE size IN (100,300)`, `size in (100, 300)`},
31 | {`SELECT * WHERE size NOT IN (100,300)`, `size not in (100, 300)`},
32 | {`SELECT * WHERE name='sarah' and age!=40 and (country='LV' OR country='AU') ORDER BY age DESC limit 1`,
33 | `name = 'sarah' and age != 40 and (country = 'LV' or country = 'AU') order by age desc LIMIT 1 OFFSET 0`},
34 | }
35 |
36 | for _, table := range tables {
37 | // Executed by the main service
38 | ast, err := sqlparser.Parse(table.sql)
39 | if err != nil {
40 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
41 | continue
42 | }
43 |
44 | stmt, ok := ast.(*sqlparser.Select)
45 | if !ok {
46 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
47 | continue
48 | }
49 |
50 | // Executed by the plugin
51 | result, err := c.convert(stmt)
52 | if err != nil {
53 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
54 | continue
55 | }
56 |
57 | if result != table.converted {
58 | t.Errorf("Invalid conversion of '%s': %v, expected: %v", table.sql, result, table.converted)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/plugins/src/file/csv/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 |
6 | "github.com/cert-lv/graphoscope/pdk"
7 | )
8 |
9 | /*
10 | * Export symbols
11 | */
12 | var (
13 | Name = "file-csv"
14 | Version = "1.0.7"
15 | Plugin plugin
16 | )
17 |
18 | /*
19 | * Structure to be imported by the core as a plugin
20 | */
21 | type plugin struct {
22 |
23 | // Inherit default configuration fields
24 | source *pdk.Source
25 |
26 | // Custom fields
27 | db *sql.DB
28 | dir string
29 | base string
30 | limit int
31 | }
32 |
--------------------------------------------------------------------------------
/plugins/src/hashlookup/README.md:
--------------------------------------------------------------------------------
1 | # Hashlookup plugin
2 |
3 | Plugin to query [hashlookup services](https://github.com/hashlookup).
4 | For instance hashlookup.circl.lu
5 |
6 | Sample command to use plugin:
7 | ```sh
8 | curl 'https://localhost:443/api?uuid=auth-key&sql=FROM+hashlookup+WHERE+sha1=%27deac5aeda66017c25a9e21f36f5ee618d2ad9d3d%27'
9 | ```
10 |
11 | Compile with:
12 | ```sh
13 | go build -buildmode=plugin -ldflags="-w" -o hashlookup.so ./*.go
14 | ```
15 | Or use the Makefile command:
16 | `make plugins-local`
17 |
18 |
19 | # Limitations
20 |
21 | Does not support complex SQL queries and datetime range selection.
22 |
23 |
24 | # Access details
25 |
26 | Source YAML definition's `access` fields:
27 | - **url**: hashlookup API endpoint, for example - `https://hashlookup.circl.lu`
28 | - **apiKey**: optional hashlookup API key
29 |
30 |
31 | # Definition file example
32 |
33 | Replace API key with your own:
34 | ```yaml
35 | name: hashlookup
36 | label: Hashlookup
37 | icon: database
38 |
39 | plugin: hashlookup
40 | inGlobal: true
41 | includeDatetime: false
42 | supportsSQL: false
43 |
44 | access:
45 | url: https://hashlookup.circl.lu
46 | apiKey: .
47 |
48 | queryFields:
49 | - md5
50 | - sha1
51 | - sha256
52 |
53 |
54 | relations:
55 | -
56 | from:
57 | id: SHA-1
58 | group: sha1
59 | search: sha1
60 | attributes: ["FileName", "FileSize", "source-url", "MD5", "SHA-512", "SHA-256", "SSDEEP", "TLSH", "insert-timestamp", "mimetype", "source", "hashlookup-parent-total", "snap-authority", "hashlookup:trust"]
61 |
62 | to:
63 | id: parent
64 | group: sha1
65 | search: sha1
66 | attributes: ["FileName", "FileSize", "source-url", "MD5", "SHA-512", "SHA-256", "SSDEEP", "TLSH", "insert-timestamp", "mimetype", "source", "hashlookup-parent-total", "snap-authority", "hashlookup:trust"]
67 |
68 | edge:
69 | label: ChildOf
70 |
71 | -
72 | from:
73 | id: SHA-1
74 | group: sha1
75 | search: sha1
76 | attributes: ["FileName", "FileSize", "source-url", "MD5", "SHA-512", "SHA-256", "SSDEEP", "TLSH", "insert-timestamp", "mimetype", "source", "hashlookup-parent-total", "snap-authority", "hashlookup:trust"]
77 |
78 | to:
79 | id: children
80 | group: sha1
81 | search: sha1
82 | attributes: ["FileName", "FileSize", "source-url", "MD5", "SHA-512", "SHA-256", "SSDEEP", "TLSH", "insert-timestamp", "mimetype", "source", "hashlookup-parent-total", "snap-authority", "hashlookup:trust"]
83 |
84 | edge:
85 | label: ParentOf
86 |
87 | -
88 | from:
89 | id: SHA-1
90 | group: sha1
91 | search: sha1
92 | attributes: ["FileName", "FileSize", "source-url", "SHA-512", "SHA-256", "SSDEEP", "TLSH", "insert-timestamp", "mimetype", "source", "hashlookup-parent-total", "snap-authority", "hashlookup:trust"]
93 |
94 | to:
95 | id: MD5
96 | group: md5
97 | search: md5
98 | attributes: ["FileName", "FileSize", "source-url", "SHA-512", "SHA-256", "SSDEEP", "TLSH", "insert-timestamp", "mimetype", "source", "hashlookup-parent-total", "snap-authority", "hashlookup:trust"]
99 |
100 | -
101 | from:
102 | id: SHA-1
103 | group: sha1
104 | search: sha1
105 | attributes: ["FileName", "FileSize", "source-url", "MD5", "SHA-512", "SSDEEP", "TLSH", "insert-timestamp", "mimetype", "source", "hashlookup-parent-total", "snap-authority", "hashlookup:trust"]
106 |
107 | to:
108 | id: SHA-256
109 | group: sha256
110 | search: sha256
111 | attributes: ["FileName", "FileSize", "source-url", "MD5", "SHA-512", "SSDEEP", "TLSH", "insert-timestamp", "mimetype", "source", "hashlookup-parent-total", "snap-authority", "hashlookup:trust"]
112 | ```
113 |
--------------------------------------------------------------------------------
/plugins/src/hashlookup/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to MongoDB query convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "errors"
9 | "github.com/blastrain/vitess-sqlparser/sqlparser"
10 | )
11 |
12 | /*
13 | * Convert SQL statement to the object expected by the data source
14 | */
15 | func (p *plugin) convert(sel *sqlparser.Select) ([][2]string, error) {
16 |
17 | // Handle WHERE.
18 | // Top level node pass in an empty interface
19 | // to tell the children this is root.
20 | // Is there any better way?
21 | var rootParent sqlparser.Expr
22 |
23 | // List of requested fields & values
24 | fields, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | // Handle GROUP BY
30 | if len(sel.GroupBy) > 0 || checkNeedAgg(sel.SelectExprs) {
31 | return nil, errors.New("'GROUP BY' & aggregation are not supported")
32 | }
33 |
34 | // Set LIMIT
35 | if sel.Limit != nil {
36 | fields = append(fields, [2]string{"limit", sqlparser.String(sel.Limit.Rowcount)})
37 | }
38 |
39 | return fields, nil
40 | }
41 |
--------------------------------------------------------------------------------
/plugins/src/hashlookup/hashlookup_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted [][2]string
21 | }{
22 | {`SELECT * WHERE sha1='DEAC5AEDA66017C25A9E21F36F5EE618D2AD9D3D'`, [][2]string{[2]string{"sha1", "DEAC5AEDA66017C25A9E21F36F5EE618D2AD9D3D"}}},
23 | }
24 |
25 | for _, table := range tables {
26 | // Executed by the main service
27 | ast, err := sqlparser.Parse(table.sql)
28 | if err != nil {
29 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
30 | continue
31 | }
32 |
33 | stmt, ok := ast.(*sqlparser.Select)
34 | if !ok {
35 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
36 | continue
37 | }
38 |
39 | // Executed by the plugin
40 | result, err := c.convert(stmt)
41 | if err != nil {
42 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
43 | continue
44 | }
45 |
46 | if !equal(result, table.converted) {
47 | t.Errorf("Invalid conversion of \"%s\": \"%s\", expected: \"%s\"", table.sql, result, table.converted)
48 | }
49 | }
50 | }
51 |
52 | /*
53 | * Check whether a and b slices contain the same elements
54 | */
55 | func equal(a, b [][2]string) bool {
56 | if len(a) != len(b) {
57 | return false
58 | }
59 |
60 | for i, v := range a {
61 | if v != b[i] {
62 | return false
63 | }
64 | }
65 |
66 | return true
67 | }
68 |
--------------------------------------------------------------------------------
/plugins/src/hashlookup/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Export symbols
9 | */
10 | var (
11 | Name = "hashlookup"
12 | Version = "1.0.1"
13 | Plugin plugin
14 | )
15 |
16 | /*
17 | * Structure to be imported by the core as a plugin
18 | */
19 | type plugin struct {
20 |
21 | // Inherit default configuration fields
22 | source *pdk.Source
23 |
24 | // Custom fields
25 | url string
26 | apiKey string
27 | limit int
28 | }
29 |
--------------------------------------------------------------------------------
/plugins/src/http/README.md:
--------------------------------------------------------------------------------
1 | # HTTP plugin
2 |
3 | HTTP connector sends a GET/POST request and expects a `[{...},{...},{...}]` list of flat JSON objects back.
4 |
5 | Request contains:
6 | - a list of `field=value` in case HTTP API doesn't support SQL queries
7 | - `sql` field with a complete SQL query in case API supports it
8 |
9 | `curl` to test:
10 | ```sh
11 | curl 'https://localhost:443/api?uuid=auth-key&sql=FROM+service+WHERE+ip=%278.8.8.8%27'
12 | ```
13 |
14 | Compile with:
15 | ```sh
16 | go build -buildmode=plugin -ldflags="-w" -o http.so ./*.go
17 | ```
18 |
19 | # Access details
20 |
21 | Source YAML definition's `access` fields:
22 | - **url**: HTTP access point, for example - `http://localhost:8000`
23 | - **method**: `GET` or `POST`, GET by default
24 |
--------------------------------------------------------------------------------
/plugins/src/http/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to the field/value list convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "errors"
9 |
10 | "github.com/blastrain/vitess-sqlparser/sqlparser"
11 | )
12 |
13 | /*
14 | * Convert SQL query to the list of [field,value]
15 | */
16 | func (p *plugin) convert(sel *sqlparser.Select) ([][2]string, error) {
17 |
18 | // Handle WHERE.
19 | // Top level node pass in an empty interface
20 | // to tell the children this is root.
21 | // Is there any better way?
22 | var rootParent sqlparser.Expr
23 |
24 | // List of requested fields & values
25 | fields, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | // Handle GROUP BY
31 | if len(sel.GroupBy) > 0 || checkNeedAgg(sel.SelectExprs) {
32 | return nil, errors.New("'GROUP BY' & aggregation are not supported")
33 | }
34 |
35 | // Handle WHERE,
36 | // will be a textual representation of the whole query
37 | query := sqlparser.String(sel.Where.Expr)
38 |
39 | // Handle ORDER BY
40 | if sel.OrderBy != nil {
41 | query += sqlparser.String(sel.OrderBy)
42 | }
43 |
44 | // Set selection OFFSET and ROWCOUNT
45 | if sel.Limit != nil {
46 | if sel.Limit.Offset != nil {
47 | fields = append(fields, [2]string{"offset", sqlparser.String(sel.Limit.Offset)})
48 | query += " LIMIT " + sqlparser.String(sel.Limit.Offset) + "," + sqlparser.String(sel.Limit.Rowcount)
49 | } else {
50 | fields = append(fields, [2]string{"offset", "0"})
51 | query += " LIMIT 0," + sqlparser.String(sel.Limit.Rowcount)
52 | }
53 |
54 | fields = append(fields, [2]string{"rowcount", sqlparser.String(sel.Limit.Rowcount)})
55 | }
56 |
57 | fields = append(fields, [2]string{"sql", query})
58 | return fields, nil
59 | }
60 |
--------------------------------------------------------------------------------
/plugins/src/http/http_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted [][2]string
21 | }{
22 | {`SELECT * WHERE ip='10.10.10.10' LIMIT 5,1`, [][2]string{
23 | [2]string{"ip", "10.10.10.10"},
24 | [2]string{"offset", "5"},
25 | [2]string{"rowcount", "1"},
26 | [2]string{"sql", "ip = '10.10.10.10' LIMIT 5,1"},
27 | }},
28 | {`SELECT * WHERE size BETWEEN 100 AND 300`, [][2]string{
29 | [2]string{"size_from", "100"},
30 | [2]string{"size_to", "300"},
31 | [2]string{"sql", "size between 100 and 300"},
32 | }},
33 | {`select * where name='sarah' and age=40 limit 1`, [][2]string{
34 | [2]string{"name", "sarah"},
35 | [2]string{"age", "40"},
36 | [2]string{"operator", "and"},
37 | [2]string{"offset", "0"},
38 | [2]string{"rowcount", "1"},
39 | [2]string{"sql", "name = 'sarah' and age = 40 LIMIT 0,1"},
40 | }},
41 | }
42 |
43 | for _, table := range tables {
44 | // Executed by the main service
45 | ast, err := sqlparser.Parse(table.sql)
46 | if err != nil {
47 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
48 | continue
49 | }
50 |
51 | stmt, ok := ast.(*sqlparser.Select)
52 | if !ok {
53 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
54 | continue
55 | }
56 |
57 | // Executed by the plugin
58 | result, err := c.convert(stmt)
59 | if err != nil {
60 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
61 | continue
62 | }
63 |
64 | if !equal(result, table.converted) {
65 | t.Errorf("Invalid conversion of '%s': %v, expected: %v", table.sql, result, table.converted)
66 | }
67 | }
68 | }
69 |
70 | /*
71 | * Check whether a and b slices contain the same elements
72 | */
73 | func equal(a, b [][2]string) bool {
74 | if len(a) != len(b) {
75 | return false
76 | }
77 | for i, v := range a {
78 | if v != b[i] {
79 | return false
80 | }
81 | }
82 | return true
83 | }
84 |
--------------------------------------------------------------------------------
/plugins/src/http/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Export symbols
9 | */
10 | var (
11 | Name = "http"
12 | Version = "1.0.5"
13 | Plugin plugin
14 | )
15 |
16 | /*
17 | * Structure to be imported by the core as a plugin
18 | */
19 | type plugin struct {
20 |
21 | // Inherit default configuration fields
22 | source *pdk.Source
23 |
24 | // Custom fields
25 | url string
26 | method string
27 | limit int
28 | }
29 |
--------------------------------------------------------------------------------
/plugins/src/ipinfo/README.md:
--------------------------------------------------------------------------------
1 | # ipinfo.io plugin
2 |
3 | Connector sends a GET request to the `ipinfo.io` API and expects an JSON response with IP details back.
4 | Request can contain `ip` field only with an IP address to check.
5 |
6 | `curl` to test:
7 | ```sh
8 | curl 'https://localhost:443/api?uuid=auth-key&sql=FROM+ipinfo+WHERE+ip=%278.8.8.8%27'
9 | ```
10 |
11 | Compile with:
12 | ```sh
13 | go build -buildmode=plugin -ldflags="-w" -o ipinfo.so ./*.go
14 | ```
15 |
16 | # Limitations
17 |
18 | Does not support complex SQL queries and datetime range selection.
19 |
20 |
21 | # Access details
22 |
23 | Source YAML definition's `access` fields:
24 | - **server**: API server, for example - `https://ipinfo.io`
25 | - **token**: User's access token, for extended queries limit
26 |
27 |
28 | # Definition file example
29 |
30 | Replace API token with your own:
31 | ```yaml
32 | name: ipinfo
33 | label: ipinfo.io
34 | icon: database
35 |
36 | plugin: ipinfo
37 | inGlobal: true
38 | includeDatetime: false
39 | supportsSQL: false
40 |
41 | access:
42 | server: https://ipinfo.io
43 | token: xxxxxxxxxxxxxx
44 |
45 | queryFields:
46 | - ip
47 |
48 |
49 | relations:
50 | -
51 | from:
52 | id: ip
53 | group: ip
54 | search: ip
55 | attributes: ["anycast", "city", "loc", "postal", "region"]
56 |
57 | to:
58 | id: hostname
59 | group: domain
60 | search: domain
61 |
62 | -
63 | from:
64 | id: ip
65 | group: ip
66 | search: ip
67 | attributes: ["anycast", "city", "loc", "postal", "region"]
68 |
69 | to:
70 | id: org
71 | group: institution
72 | search: institution
73 | attributes: ["asn", "city", "loc", "postal", "region"]
74 | ```
--------------------------------------------------------------------------------
/plugins/src/ipinfo/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to the field/value list convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "github.com/blastrain/vitess-sqlparser/sqlparser"
9 | )
10 |
11 | /*
12 | * Convert SQL query to the list of [field,value]
13 | */
14 | func (p *plugin) convert(sel *sqlparser.Select) ([2]string, error) {
15 |
16 | // Handle WHERE.
17 | // Top level node pass in an empty interface
18 | // to tell the children this is root.
19 | // Is there any better way?
20 | var rootParent sqlparser.Expr
21 |
22 | // Requested field & value
23 | field, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
24 | if err != nil {
25 | return [2]string{}, err
26 | }
27 |
28 | return field, nil
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/src/ipinfo/ipinfo_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted [2]string
21 | }{
22 | {`SELECT * WHERE ip='8.8.8.8'`, [2]string{"ip", "8.8.8.8"}},
23 | }
24 |
25 | for _, table := range tables {
26 | // Executed by the main service
27 | ast, err := sqlparser.Parse(table.sql)
28 | if err != nil {
29 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
30 | continue
31 | }
32 |
33 | stmt, ok := ast.(*sqlparser.Select)
34 | if !ok {
35 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
36 | continue
37 | }
38 |
39 | // Executed by the plugin
40 | result, err := c.convert(stmt)
41 | if err != nil {
42 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
43 | continue
44 | }
45 |
46 | if !equal(result, table.converted) {
47 | t.Errorf("Invalid conversion of '%s': %v, expected: %v", table.sql, result, table.converted)
48 | }
49 | }
50 | }
51 |
52 | /*
53 | * Check whether a and b slices contain the same elements
54 | */
55 | func equal(a, b [2]string) bool {
56 | if a[0] != b[0] || a[1] != b[1] {
57 | return false
58 | }
59 |
60 | return true
61 | }
62 |
--------------------------------------------------------------------------------
/plugins/src/ipinfo/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Export symbols
9 | */
10 | var (
11 | Name = "ipinfo"
12 | Version = "1.0.1"
13 | Plugin plugin
14 | )
15 |
16 | /*
17 | * Structure to be imported by the core as a plugin
18 | */
19 | type plugin struct {
20 |
21 | // Inherit default configuration fields
22 | source *pdk.Source
23 |
24 | // Custom fields
25 | server string
26 | token string
27 | limit int
28 | }
29 |
--------------------------------------------------------------------------------
/plugins/src/ipinfo/select.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "strings"
7 |
8 | "github.com/blastrain/vitess-sqlparser/sqlparser"
9 | )
10 |
11 | /*
12 | * Handle single "field operator value" expression.
13 | *
14 | * Receives:
15 | * expr - SQL expression to process
16 | * topLevel - whether it's a top level expression
17 | * parent - container of the expression
18 | */
19 | func handleSelectWhereComparisonExpr(expr *sqlparser.Expr, topLevel bool, parent *sqlparser.Expr) ([2]string, error) {
20 | comparisonExpr := (*expr).(*sqlparser.ComparisonExpr)
21 | colName, ok := comparisonExpr.Left.(*sqlparser.ColName)
22 |
23 | if !ok {
24 | return [2]string{}, errors.New("Invalid comparison expression, the left must be a column name")
25 | }
26 |
27 | colNameStr := sqlparser.String(colName)
28 | colNameStr = strings.Replace(colNameStr, "`", "", -1)
29 | rightIntf, err := buildComparisonExprRightStr(comparisonExpr.Right)
30 | if err != nil {
31 | return [2]string{}, err
32 | }
33 |
34 | if comparisonExpr.Operator == "=" {
35 | return [2]string{colNameStr, fmt.Sprintf("%s", rightIntf)}, nil
36 | }
37 |
38 | return [2]string{}, errors.New("'=' operator is supported only")
39 | }
40 |
41 | /*
42 | * Handle top level or groups of expressions.
43 | *
44 | * Receives:
45 | * expr - SQL expression to process
46 | * topLevel - whether it's a top level expression
47 | * parent - container of the expression
48 | */
49 | func handleSelectWhereParenExpr(expr *sqlparser.Expr, topLevel bool, parent *sqlparser.Expr) ([2]string, error) {
50 | parentBoolExpr := (*expr).(*sqlparser.ParenExpr)
51 | boolExpr := parentBoolExpr.Expr
52 |
53 | // If parent is the top level, bool must is needed
54 | var isThisTopLevel = false
55 | if topLevel {
56 | isThisTopLevel = true
57 | }
58 |
59 | return handleSelectWhere(&boolExpr, isThisTopLevel, parent)
60 | }
61 |
62 | /*
63 | * Check the right part of the expression
64 | * and return its value of specific type.
65 | *
66 | * Receives SQL expression to process
67 | */
68 | func buildComparisonExprRightStr(expr sqlparser.Expr) (interface{}, error) {
69 | var rightStr string
70 | var err error
71 |
72 | switch expr := expr.(type) {
73 | case *sqlparser.SQLVal:
74 | // Use string value type only
75 | rightStr = sqlparser.String(expr)
76 | rightStr = strings.Trim(rightStr, "'")
77 |
78 | case *sqlparser.BoolVal, sqlparser.BoolVal:
79 | rightStr = sqlparser.String(expr)
80 |
81 | case *sqlparser.GroupConcatExpr:
82 | return nil, errors.New("group_concat not supported")
83 |
84 | case *sqlparser.FuncExpr:
85 | return nil, errors.New("functions are not supported")
86 |
87 | case *sqlparser.ColName:
88 | if sqlparser.String(expr) == "exist" {
89 | return nil, errors.New("'exist' expression currently not supported")
90 | }
91 | return nil, errors.New("Column name on the right side of compare operator is not supported")
92 |
93 | case sqlparser.ValTuple:
94 | rightStr = sqlparser.String(expr)
95 |
96 | default:
97 | return nil, fmt.Errorf("Unexpected SQL expression right part's type: %T", expr)
98 | }
99 |
100 | return rightStr, err
101 | }
102 |
103 | /*
104 | * Handle WHERE statement.
105 | *
106 | * Receives:
107 | * expr - SQL expression to process
108 | * topLevel - whether it's a top level expression
109 | * parent - container of the expression
110 | */
111 | func handleSelectWhere(expr *sqlparser.Expr, topLevel bool, parent *sqlparser.Expr) ([2]string, error) {
112 | if expr == nil {
113 | return [2]string{}, errors.New("SQL expression cannot be nil here")
114 | }
115 |
116 | switch (*expr).(type) {
117 | case *sqlparser.ComparisonExpr:
118 | return handleSelectWhereComparisonExpr(expr, topLevel, parent)
119 |
120 | case *sqlparser.ParenExpr:
121 | return handleSelectWhereParenExpr(expr, topLevel, parent)
122 | }
123 |
124 | return [2]string{}, fmt.Errorf("Unexpected SQL expression type received: %T", *expr)
125 | }
126 |
--------------------------------------------------------------------------------
/plugins/src/misp/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to the field/value list convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "github.com/blastrain/vitess-sqlparser/sqlparser"
9 | )
10 |
11 | /*
12 | * Convert SQL query to the [field, value] or
13 | * [field, value, datetime from, datetime to] if datetime exists
14 | */
15 | func (p *plugin) convert(sel *sqlparser.Select) ([]string, error) {
16 |
17 | // Handle WHERE.
18 | // Top level node pass in an empty interface
19 | // to tell the children this is root.
20 | // Is there any better way?
21 | var rootParent sqlparser.Expr
22 |
23 | // List of requested fields & values
24 | fields, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
25 | if err != nil {
26 | return nil, err
27 | }
28 |
29 | return fields, nil
30 | }
31 |
--------------------------------------------------------------------------------
/plugins/src/misp/misp_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted []string
21 | }{
22 | {
23 | "SELECT * WHERE `domain|ip`='10.10.10.10' LIMIT 5,1",
24 | []string{"domain|ip", "10.10.10.10"},
25 | },
26 | {
27 | `SELECT * WHERE ip='10.10.10.10' and datetime BETWEEN '2024-05-04T11:30:14.000Z' AND '2024-06-04T11:30:14.000Z'`,
28 | []string{"ip", "10.10.10.10", "2024-05-04T11:30:14.000Z", "2024-06-04T11:30:14.000Z"},
29 | },
30 | {
31 | `select * where name='sarah'`,
32 | []string{"name", "sarah"},
33 | },
34 | }
35 |
36 | for _, table := range tables {
37 | // Executed by the main service
38 | ast, err := sqlparser.Parse(table.sql)
39 | if err != nil {
40 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
41 | continue
42 | }
43 |
44 | stmt, ok := ast.(*sqlparser.Select)
45 | if !ok {
46 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
47 | continue
48 | }
49 |
50 | // Executed by the plugin
51 | result, err := c.convert(stmt)
52 | if err != nil {
53 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
54 | continue
55 | }
56 |
57 | if !equal(result, table.converted) {
58 | t.Errorf("Invalid conversion of '%s': %v, expected: %v", table.sql, result, table.converted)
59 | }
60 | }
61 | }
62 |
63 | /*
64 | * Check whether a and b slices contain the same elements
65 | */
66 | func equal(a, b []string) bool {
67 | if len(a) != len(b) {
68 | return false
69 | }
70 |
71 | for i, v := range a {
72 | if v != b[i] {
73 | return false
74 | }
75 | }
76 |
77 | return true
78 | }
79 |
--------------------------------------------------------------------------------
/plugins/src/misp/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Export symbols
9 | */
10 | var (
11 | Name = "misp"
12 | Version = "1.0.1"
13 | Plugin plugin
14 | )
15 |
16 | /*
17 | * Structure to be imported by the core as a plugin
18 | */
19 | type plugin struct {
20 |
21 | // Inherit default configuration fields
22 | source *pdk.Source
23 |
24 | // Custom fields
25 | protocol string
26 | host string
27 | apiKey string
28 | caCertPath string
29 | certPath string
30 | keyPath string
31 | types map[string]bool
32 | limit int
33 | }
34 |
--------------------------------------------------------------------------------
/plugins/src/modify/README.md:
--------------------------------------------------------------------------------
1 | # Modify plugin
2 |
3 | Allows to modify parameters of existing graph elements.
4 |
5 |
6 | Compile with:
7 | ```sh
8 | go build -buildmode=plugin -ldflags="-w" -o modify.so ./*.go
9 | ```
10 |
11 | # Configuration
12 |
13 | YAML definition's `data` fields:
14 | - **group**: as all graph nodes belong to some group/type, it can be used to apply modifications to the specific group only
15 | - **modify**: list of replacements. If **regex** matches node/edge's **field** value - put **replacement** instead
16 |
17 | More info about used function: https://pkg.go.dev/regexp#Regexp.ReplaceAllString,
18 | where:
19 | - `MustCompile` receives regex
20 | - `ReplaceAllString` receives graph field value and replacement
21 |
22 | List of replacements example:
23 | ```yaml
24 | modify:
25 | - field: field_name
26 | regex: a(x*)b
27 | replacement: y
28 |
29 | # Anonymize prices
30 | - field: price
31 | regex: (?i)\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{2}) *(eur|€)
32 | replacement: *** Eur
33 | ```
34 |
--------------------------------------------------------------------------------
/plugins/src/modify/modify.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "regexp"
6 |
7 | "github.com/cert-lv/graphoscope/pdk"
8 | )
9 |
10 | /*
11 | * Check "pdk/plugin.go" for the built-in plugin functions description
12 | */
13 |
14 | func (p *plugin) Conf() *pdk.Processor {
15 | return p.processor
16 | }
17 |
18 | func (p *plugin) Setup(processor *pdk.Processor) error {
19 |
20 | // Store settings
21 | p.processor = processor
22 |
23 | // Convert string regexs into actual regexps
24 | if p.processor.Data["modify"] != nil {
25 | for i, entry := range p.processor.Data["modify"].([]interface{}) {
26 | p.processor.Data["modify"].([]interface{})[i].(map[string]interface{})["regex"] = regexp.MustCompile(entry.(map[string]interface{})["regex"].(string))
27 | }
28 | }
29 |
30 | return nil
31 | }
32 |
33 | func (p *plugin) Process(relations []map[string]interface{}) ([]map[string]interface{}, error) {
34 |
35 | if p.processor.Data["modify"] != nil {
36 | for _, relation := range relations {
37 | for _, m := range p.processor.Data["modify"].([]interface{}) {
38 | for _, part := range []string{"from", "to", "edge"} {
39 | rp := relation[part]
40 |
41 | if rp != nil {
42 | rpt := relation[part].(map[string]interface{})
43 |
44 | if p.processor.Data["group"] != nil && rpt["group"] != p.processor.Data["group"].(string) {
45 | continue
46 | }
47 |
48 | mt := m.(map[string]interface{})
49 |
50 | if mt["field"].(string) == "id" {
51 | rpt["id"] = mt["regex"].(*regexp.Regexp).ReplaceAllString(rpt["id"].(string), fmt.Sprint(mt["replacement"]))
52 | }
53 |
54 | if rpt["attributes"] != nil && rpt["attributes"].(map[string]interface{})[mt["field"].(string)] != nil {
55 | rpt["attributes"].(map[string]interface{})[mt["field"].(string)] = mt["regex"].(*regexp.Regexp).ReplaceAllString(rpt["attributes"].(map[string]interface{})[mt["field"].(string)].(string), fmt.Sprint(mt["replacement"]))
56 | }
57 | }
58 | }
59 | }
60 | }
61 | }
62 |
63 | return relations, nil
64 | }
65 |
66 | func (p *plugin) Stop() error {
67 | return nil
68 | }
69 |
--------------------------------------------------------------------------------
/plugins/src/modify/modify_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/cert-lv/graphoscope/pdk"
7 | )
8 |
9 | /*
10 | * Test attributes modifications
11 | */
12 | func TestProcess(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := &plugin{}
16 |
17 | processor := &pdk.Processor{
18 | Data: map[string]interface{}{
19 | "group": "name",
20 |
21 | "modify": []interface{}{
22 | map[string]interface{}{
23 | "field": "age",
24 | "regex": "\\d*",
25 | "replacement": "***",
26 | },
27 | },
28 | },
29 | }
30 |
31 | err := c.Setup(processor)
32 | if err != nil {
33 | t.Errorf("Can't setup a modify plugin: %s", err.Error())
34 | }
35 |
36 | // Pairs of data source plugins responses and the expected processing results
37 | table := []struct {
38 | entry map[string]interface{}
39 | modified map[string]interface{}
40 | }{
41 | // No modifications should be made, because group is different
42 | {map[string]interface{}{
43 | "from": map[string]interface{}{
44 | "id": "John",
45 | "group": "neighbor",
46 | "search": "name",
47 | "attributes": map[string]interface{}{
48 | "age": "25",
49 | },
50 | },
51 | }, map[string]interface{}{
52 | "from": map[string]interface{}{
53 | "id": "John",
54 | "group": "name",
55 | "search": "name",
56 | "attributes": map[string]interface{}{
57 | "age": "25",
58 | },
59 | },
60 | }},
61 |
62 | // Age should be anonymized
63 | {map[string]interface{}{
64 | "from": map[string]interface{}{
65 | "id": "John",
66 | "group": "name",
67 | "search": "name",
68 | "attributes": map[string]interface{}{
69 | "age": "25",
70 | },
71 | },
72 | }, map[string]interface{}{
73 | "from": map[string]interface{}{
74 | "id": "John",
75 | "group": "name",
76 | "search": "name",
77 | "attributes": map[string]interface{}{
78 | "age": "***",
79 | },
80 | },
81 | }},
82 | }
83 |
84 | for _, row := range table {
85 | result, err := c.Process([]map[string]interface{}{row.entry})
86 | if err != nil {
87 | t.Errorf("Can't process '%s': %s", row.entry, err.Error())
88 | continue
89 | }
90 |
91 | modified := row.modified["from"].(map[string]interface{})
92 | from := result[0]["from"].(map[string]interface{})
93 |
94 | if from["id"] != modified["id"] {
95 | t.Errorf("Invalid modification of ID in \"%v\": \"%v\", expected: \"%v\"",
96 | row.entry["from"], from, modified)
97 | break
98 | }
99 |
100 | for k, v := range from["attributes"].(map[string]interface{}) {
101 | if modified["attributes"].(map[string]interface{})[k] != v {
102 | t.Errorf("Invalid modification of \"%v\": \"%v\", expected: \"%v\"",
103 | row.entry["from"], from, modified)
104 | break
105 | }
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/plugins/src/modify/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Export symbols
9 | */
10 | var (
11 | Name = "modify"
12 | Version = "1.0.0"
13 | Plugin plugin
14 | )
15 |
16 | /*
17 | * Structure to be imported by the core as a plugin
18 | */
19 | type plugin struct {
20 |
21 | // Inherit default configuration fields
22 | processor *pdk.Processor
23 | }
24 |
--------------------------------------------------------------------------------
/plugins/src/mongodb/README.md:
--------------------------------------------------------------------------------
1 | # MongoDB plugin
2 |
3 | Plugin to query MongoDB (https://www.mongodb.com) as a data source.
4 |
5 |
6 | Compile with:
7 | ```sh
8 | go build -buildmode=plugin -ldflags="-w" -o mongodb.so ./*.go
9 | ```
10 |
11 | # Access details
12 |
13 | Source YAML definition's `access` fields:
14 | - **addr**: HOST:PORT database's access point, for example - `localhost:27017`
15 | - **db**: database name to use
16 | - **collection**: collection name to query
17 |
18 |
19 | # Get all the possible fields
20 |
21 | As MongoDB doesn't provide a built-in way to get all the possible collection's
22 | fields without requesting all documents plugin will try:
23 | 1. To return a manually filled `queryFields` - useful when some fields rarely appear
24 | 2. To request 1000 documents and get all their unique fields, including nested
25 |
26 |
27 | # Golang driver
28 |
29 | The **mongo-go-driver** contains four object types:
30 |
31 | - **bson.D**: A BSON document. This type should be used in situations where order matters, such as MongoDB commands
32 | - **bson.M**: An unordered map. It is the same as D, except it does not preserve order
33 | - **bson.A**: A BSON array
34 | - **bson.E**: A single element inside a D
35 |
--------------------------------------------------------------------------------
/plugins/src/mongodb/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to MongoDB query convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "errors"
9 | "strconv"
10 | "strings"
11 |
12 | "github.com/blastrain/vitess-sqlparser/sqlparser"
13 | "go.mongodb.org/mongo-driver/bson"
14 | "go.mongodb.org/mongo-driver/mongo/options"
15 | )
16 |
17 | /*
18 | * Convert SQL statement to the MongoDB filter & options
19 | */
20 | func (p *plugin) convert(sel *sqlparser.Select, fields []string) (bson.M, *options.FindOptions, error) {
21 |
22 | // Handle WHERE.
23 | // Top level node pass in an empty interface
24 | // to tell the children this is root.
25 | // Is there any better way?
26 | var rootParent sqlparser.Expr
27 |
28 | // Values to search for
29 | filter, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
30 | if err != nil {
31 | return nil, nil, err
32 | }
33 |
34 | // Handle GROUP BY
35 | if len(sel.GroupBy) > 0 || checkNeedAgg(sel.SelectExprs) {
36 | return nil, nil, errors.New("'GROUP BY' & aggregation are not supported")
37 | }
38 |
39 | // Include required fields only
40 | projection := bson.D{}
41 | for _, field := range fields {
42 | projection = append(projection, bson.E{Key: field, Value: 1})
43 | }
44 |
45 | // Pass these options to the Find method
46 | options := options.Find().SetProjection(projection)
47 |
48 | // Offset & Rowcount validation is done by the core service
49 | if sel.Limit != nil {
50 | // Handle offset
51 | if sel.Limit.Offset != nil {
52 | offset := sqlparser.String(sel.Limit.Offset)
53 | queryFrom, _ := strconv.ParseInt(offset, 10, 64)
54 | options.SetSkip(queryFrom)
55 | } else {
56 | options.SetSkip(0)
57 | }
58 |
59 | // Handle limit
60 | rowcount := sqlparser.String(sel.Limit.Rowcount)
61 | querySize, _ := strconv.ParseInt(rowcount, 10, 64)
62 | options.SetLimit(querySize)
63 | }
64 |
65 | // Handle ORDER BY
66 | var orderByArr bson.D
67 | for _, orderByExpr := range sel.OrderBy {
68 | direction := 1
69 | if orderByExpr.Direction == "desc" {
70 | direction = -1
71 | }
72 |
73 | orderByArr = append(orderByArr, bson.E{Key: strings.Replace(sqlparser.String(orderByExpr.Expr), "`", "", -1), Value: direction})
74 | }
75 |
76 | if len(orderByArr) > 0 {
77 | options.SetSort(orderByArr)
78 | }
79 |
80 | return filter, options, nil
81 | }
82 |
--------------------------------------------------------------------------------
/plugins/src/mongodb/mongodb_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/blastrain/vitess-sqlparser/sqlparser"
8 | "go.mongodb.org/mongo-driver/bson/primitive"
9 | )
10 |
11 | /*
12 | * Test SQL conversion to the data source's expected format
13 | */
14 | func TestConvert(t *testing.T) {
15 |
16 | // Empty plugin's instance to test
17 | c := plugin{}
18 |
19 | // Pairs of SQLs and the expected results
20 | tables := []struct {
21 | sql string
22 | filter string
23 | sort string
24 | skip int64
25 | limit int64
26 | }{
27 | {`SELECT * WHERE ip='10.10.10.10'`, `primitive.M{"ip":"10.10.10.10"}`, `nil`, 0, 0},
28 | {`SELECT * WHERE ip='10.10.10.10' LIMIT 5,10`, `primitive.M{"ip":"10.10.10.10"}`, `nil`, 5, 10},
29 | {`SELECT * WHERE size>100 ORDER BY name LIMIT 0,1`, `primitive.M{"size":primitive.M{"$gt":100}}`, `primitive.M{"name":1}`, 0, 1},
30 | {`SELECT * WHERE size=10 ORDER BY name DESC LIMIT 0,1`, `primitive.M{"size":10}`, `primitive.M{"name":-1}`, 0, 1},
31 | {`SELECT * WHERE size>=100`, `primitive.M{"size":primitive.M{"$gte":100}}`, `nil`, 0, 1},
32 | {`SELECT * WHERE name LIKE 's%'`, `primitive.M{"name":primitive.M{"$regex":primitive.Regex{Pattern:"s.*", Options:"i"}}}`, `nil`, 0, 1},
33 | {`SELECT * WHERE name NOT LIKE 's%'`, `primitive.M{"name":primitive.M{"$not":primitive.M{"$regex":primitive.Regex{Pattern:"s.*", Options:"i"}}}}`, `nil`, 0, 1},
34 | {`SELECT * WHERE size BETWEEN 100 AND 300`, `primitive.M{"size":primitive.M{"$gte":100, "$lte":300}}`, `nil`, 0, 1},
35 | {`SELECT * WHERE size NOT BETWEEN 1 AND 10`, `primitive.M{"size":primitive.M{"$gt":10, "$lt":1}}`, `nil`, 0, 1},
36 | {`SELECT * WHERE size IN (100,300)`, `primitive.M{"size":primitive.M{"$in":primitive.A{100, 300}}}`, `nil`, 0, 1},
37 | {`SELECT * WHERE size NOT IN (100,300)`, `primitive.M{"size":primitive.M{"$nin":primitive.A{100, 300}}}`, `nil`, 0, 1},
38 | {`select * where name='sarah' and age!=40 and (country='LV' or country='AU') limit 1`, `primitive.M{"$and":primitive.A{primitive.M{"$and":primitive.A{primitive.M{"name":"sarah"}, primitive.M{"age":primitive.M{"$ne":40}}}}, primitive.M{"$or":primitive.A{primitive.M{"country":"LV"}, primitive.M{"country":"AU"}}}}}`, `nil`, 0, 1},
39 | }
40 |
41 | for _, table := range tables {
42 | // Executed by the main service
43 | ast, err := sqlparser.Parse(table.sql)
44 | if err != nil {
45 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
46 | continue
47 | }
48 |
49 | stmt, ok := ast.(*sqlparser.Select)
50 | if !ok {
51 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
52 | continue
53 | }
54 |
55 | // Executed by the plugin
56 | filter, options, err := c.convert(stmt, nil)
57 | if err != nil {
58 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
59 | continue
60 | }
61 |
62 | if fmt.Sprintf("%#v", filter) != table.filter {
63 | t.Errorf("Invalid converted filter of '%s': %#v, expected: %s", table.sql, filter, table.filter)
64 | }
65 | if options.Sort != nil && fmt.Sprintf("%#v", options.Sort.(primitive.D).Map()) != table.sort {
66 | t.Errorf("Invalid converted order of '%s': %#v, expected: %s", table.sql, options.Sort.(primitive.D).Map(), table.sort)
67 | }
68 |
69 | if options.Limit != nil {
70 | if *options.Limit != table.limit || *options.Skip != table.skip {
71 | t.Errorf("Invalid converted options of '%s': '%v,%v', expected: '%v,%v'", table.sql, *options.Skip, *options.Limit, table.skip, table.limit)
72 | }
73 | }
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/plugins/src/mongodb/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | "go.mongodb.org/mongo-driver/mongo"
6 | )
7 |
8 | /*
9 | * Export symbols
10 | */
11 | var (
12 | Name = "mongodb"
13 | Version = "1.0.6"
14 | Plugin plugin
15 | )
16 |
17 | /*
18 | * Structure to be imported by the core as a plugin
19 | */
20 | type plugin struct {
21 |
22 | // Inherit default configuration fields
23 | source *pdk.Source
24 |
25 | // Custom fields
26 | client *mongo.Client
27 | collection *mongo.Collection
28 | limit int
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/src/mysql/README.md:
--------------------------------------------------------------------------------
1 | # MySQL plugin
2 |
3 | Plugin to query MySQL (https://www.mysql.com/) as a data source.
4 |
5 |
6 | Compile with:
7 | ```sh
8 | go build -buildmode=plugin -ldflags="-w" -o mysql.so ./*.go
9 | ```
10 |
11 | **Warning**
12 |
13 | SQL doesn't allow to query missing columns, like Elasticsearch does.
14 | An error `column "X" does not exist` will be received. That means you must be
15 | very careful with designing a data source and creating a YAML config file to be
16 | able to combine it with data source types other than SQL.
17 |
18 | The easiest solution is to exclude MySQL DB from the `global` namespace
19 | and query it independently, to make sure all columns exist.
20 |
21 |
22 | # Access details
23 |
24 | Source YAML definition's `access` fields:
25 | - **addr**: HOST:PORT database's access point, for example - `localhost:3306`
26 | - **user**: username to connect to the database
27 | - **password**: user's password
28 | - **db**: database name to use
29 | - **table**: table name to query
30 |
31 |
32 | # Usage
33 |
34 | Simple example of a new MySQL data source:
35 | ```sql
36 | mysql -u root -p
37 |
38 | CREATE DATABASE mydb;
39 | CREATE USER 'graphoscope'@'%' IDENTIFIED BY 'password';
40 | GRANT SELECT ON mydb.* TO 'graphoscope'@'%';
41 | FLUSH PRIVILEGES;
42 | USE mydb;
43 | CREATE TABLE mycoll (id SERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL, username VARCHAR(255) NOT NULL, fqdn VARCHAR(255) NOT NULL, count integer NOT NULL, seen TIMESTAMP);
44 | INSERT INTO mycoll (email, username, fqdn, count, seen) VALUES ('a@example.com', 'a', 'example.com', 13, now());
45 | INSERT INTO mycoll (email, username, fqdn, count, seen) VALUES ('b@example.com', 'b', 'example.com', 13, now());
46 | INSERT INTO mycoll (email, username, fqdn, count, seen) VALUES ('c@example.com', 'c', 'example.com', 13, now());
47 | INSERT INTO mycoll (email, username, fqdn, count, seen) VALUES ('d@example.com', 'd', 'example.com', 13, now());
48 | INSERT INTO mycoll (email, username, fqdn, count, seen) VALUES ('e@example.com', 'e', 'example.com', 13, now());
49 | ```
50 |
51 | Access data will be used by the source's YAML definition. Example:
52 | ```yaml
53 | name: mytest
54 | label: MYTest
55 | icon: database
56 |
57 | plugin: mysql
58 | inGlobal: false
59 | includeDatetime: false
60 | supportsSQL: true
61 |
62 | access:
63 | addr: 127.0.0.1:3306
64 | user: graphoscope
65 | password: password
66 | db: mydb
67 | table: mycoll
68 |
69 | statsFields:
70 | - domain
71 |
72 | replaceFields:
73 | datetime: seen
74 | domain: fqdn
75 |
76 |
77 | relations:
78 | -
79 | from:
80 | id: email
81 | group: email
82 | search: email
83 | attributes: [ "seen", "fqdn" ]
84 |
85 | to:
86 | id: username
87 | group: domain
88 | search: username
89 |
90 | edge:
91 | attributes: [ "count" ]
92 | ```
93 |
94 | Test with a query:
95 | ```sh
96 | curl -XGET 'https://localhost:443/api?uuid=auth-key&sql=FROM+mytest+WHERE+email+like+%27a%25%27'
97 | ```
98 |
--------------------------------------------------------------------------------
/plugins/src/mysql/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to MySQL query convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "github.com/blastrain/vitess-sqlparser/sqlparser"
9 | )
10 |
11 | /*
12 | * Convert SQL statement to the MySQL query
13 | */
14 | func (p *plugin) convert(sel *sqlparser.Select) (string, error) {
15 |
16 | // Handle WHERE
17 | query := sqlparser.String(sel.Where.Expr)
18 |
19 | // Handle GROUP BY
20 | if len(sel.GroupBy) > 0 {
21 | query += sqlparser.String(sel.GroupBy)
22 | }
23 |
24 | // Handle ORDER BY
25 | if sel.OrderBy != nil {
26 | query += sqlparser.String(sel.OrderBy)
27 | }
28 |
29 | // Handle LIMIT offset,rowcount
30 | if sel.Limit != nil {
31 | if sel.Limit.Offset != nil {
32 | query += " LIMIT " + sqlparser.String(sel.Limit.Offset) + "," + sqlparser.String(sel.Limit.Rowcount)
33 | } else {
34 | query += " LIMIT 0," + sqlparser.String(sel.Limit.Rowcount)
35 | }
36 | }
37 |
38 | return query, nil
39 | }
40 |
--------------------------------------------------------------------------------
/plugins/src/mysql/mysql_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted string
21 | }{
22 | {`SELECT * WHERE ip='10.10.10.10'`, `ip = '10.10.10.10'`},
23 | {`SELECT * WHERE ip='10.10.10.10' LIMIT 5,10`, `ip = '10.10.10.10' LIMIT 5,10`},
24 | {`SELECT * WHERE size>100 ORDER BY name LIMIT 0,1`, `size > 100 order by name asc LIMIT 0,1`},
25 | {`SELECT * WHERE size=10 ORDER BY name DESC LIMIT 0,1`, `size = 10 order by name desc LIMIT 0,1`},
26 | {`SELECT * WHERE size>=100`, `size >= 100`},
27 | {`SELECT * WHERE name LIKE 's%'`, `name like 's%'`},
28 | {`SELECT * WHERE name NOT LIKE 's%'`, `name not like 's%'`},
29 | {`SELECT * WHERE size BETWEEN 100 AND 300`, `size between 100 and 300`},
30 | {`SELECT * WHERE size IN (100,300)`, `size in (100, 300)`},
31 | {`SELECT * WHERE size NOT IN (100,300)`, `size not in (100, 300)`},
32 | {`SELECT * WHERE name='sarah' and age!=40 AND (country='LV' OR country='AU') order by age desc limit 1`,
33 | `name = 'sarah' and age != 40 and (country = 'LV' or country = 'AU') order by age desc LIMIT 0,1`},
34 | }
35 |
36 | for _, table := range tables {
37 | // Executed by the main service
38 | ast, err := sqlparser.Parse(table.sql)
39 | if err != nil {
40 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
41 | continue
42 | }
43 |
44 | stmt, ok := ast.(*sqlparser.Select)
45 | if !ok {
46 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
47 | continue
48 | }
49 |
50 | // Executed by the plugin
51 | result, err := c.convert(stmt)
52 | if err != nil {
53 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
54 | continue
55 | }
56 |
57 | if result != table.converted {
58 | t.Errorf("Invalid conversion of \"%s\": \"%s\", expected: \"%s\"", table.sql, result, table.converted)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/plugins/src/mysql/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 |
6 | "github.com/cert-lv/graphoscope/pdk"
7 | )
8 |
9 | /*
10 | * Export symbols
11 | */
12 | var (
13 | Name = "mysql"
14 | Version = "1.0.5"
15 | Plugin plugin
16 | )
17 |
18 | /*
19 | * Structure to be imported by the core as a plugin
20 | */
21 | type plugin struct {
22 |
23 | // Inherit default configuration fields
24 | source *pdk.Source
25 |
26 | // Custom fields
27 | db *sql.DB
28 | limit int
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/src/pastelyzer/README.md:
--------------------------------------------------------------------------------
1 | # Pastelyzer plugin
2 |
3 | Plugin to query Pastelyzer (https://github.com/cert-lv/pastelyzer) as a data source.
4 |
5 | Sample command to use plugin:
6 | ```sh
7 | # Get paste IDs where IP 8.8.8.8 was mentioned
8 | curl 'https://localhost:443/api?uuid=auth-key&sql=FROM+pastelyzer+WHERE+ip=%278.8.8.8%27'
9 | # Get all artefacts of the given paste ID
10 | curl 'https://localhost:443/api?uuid=auth-key&sql=FROM+pastelyzer+WHERE+source=35853628'
11 | ```
12 |
13 | As there is no way to get automatically all the possible fields to query (for the Web GUI autocomplete) - such artefacts are:
14 | - cc-number
15 | - credential
16 | - domain
17 | - email
18 | - ip
19 | - onion
20 | - sha1
21 | - any
22 |
23 | Compile with:
24 | ```sh
25 | go build -buildmode=plugin -ldflags="-w" -o pastelyzer.so ./*.go
26 | ```
27 |
28 | # Limitations
29 |
30 | Does not support complex SQL queries and datetime range selection.
31 |
32 |
33 | # Access details
34 |
35 | Source YAML definition's `access` fields:
36 | - **url**: HTTP access point, for example - `http://localhost:7000`
37 |
38 |
39 | # Definition file example
40 |
41 | ```yaml
42 | name: pastelyzer
43 | label: Pastelyzer
44 | icon: copy outline
45 |
46 | plugin: pastelyzer
47 | inGlobal: true
48 | includeDatetime: false
49 | supportsSQL: false
50 |
51 | access:
52 | url: http://127.0.0.1:7000
53 |
54 | queryFields:
55 | - source
56 | - cc-number
57 | - credential
58 | - domain
59 | - email
60 | - ip
61 | - onion
62 | - sha1
63 | - any
64 |
65 | statsFields:
66 | - ip
67 | - domain
68 | - type
69 |
70 |
71 | relations:
72 | -
73 | from:
74 | id: domain
75 | group: domain
76 | search: domain
77 |
78 | to:
79 | id: source
80 | group: paste
81 | search: source
82 |
83 | edge:
84 | label: was published
85 |
86 | -
87 | from:
88 | id: ip
89 | group: ip
90 | search: ip
91 |
92 | to:
93 | id: source
94 | group: paste
95 | search: source
96 |
97 | edge:
98 | label: was published
99 |
100 | -
101 | from:
102 | id: address
103 | group: ip
104 | search: ip
105 |
106 | to:
107 | id: source
108 | group: paste
109 | search: source
110 |
111 | edge:
112 | label: was published
113 |
114 | -
115 | from:
116 | id: cc-number
117 | group: cc-number
118 | search: cc-number
119 |
120 | to:
121 | id: source
122 | group: paste
123 | search: source
124 |
125 | edge:
126 | label: was published
127 |
128 | -
129 | from:
130 | id: credential
131 | group: credentials
132 | search: credential
133 |
134 | to:
135 | id: source
136 | group: paste
137 | search: source
138 |
139 | edge:
140 | label: was published
141 |
142 | -
143 | from:
144 | id: email
145 | group: email
146 | search: email
147 |
148 | to:
149 | id: source
150 | group: paste
151 | search: source
152 |
153 | edge:
154 | label: was published
155 |
156 | -
157 | from:
158 | id: onion
159 | group: onion
160 | search: onion
161 |
162 | to:
163 | id: source
164 | group: paste
165 | search: source
166 |
167 | edge:
168 | label: was published
169 |
170 | -
171 | from:
172 | id: sha1
173 | group: sha1
174 | search: sha1
175 |
176 | to:
177 | id: source
178 | group: paste
179 | search: source
180 |
181 | edge:
182 | label: was published
183 | ```
--------------------------------------------------------------------------------
/plugins/src/pastelyzer/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to Pastelyzer query convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "errors"
9 |
10 | "github.com/blastrain/vitess-sqlparser/sqlparser"
11 | )
12 |
13 | /*
14 | * Convert SQL query to the Pastelyzer query
15 | */
16 | func (p *plugin) convert(sel *sqlparser.Select) ([][2]string, error) {
17 |
18 | // Handle WHERE.
19 | // Top level node pass in an empty interface
20 | // to tell the children this is root.
21 | // Is there any better way?
22 | var rootParent sqlparser.Expr
23 |
24 | // List of requested fields & values
25 | fields, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
26 | if err != nil {
27 | return nil, err
28 | }
29 |
30 | // Handle GROUP BY
31 | if len(sel.GroupBy) > 0 || checkNeedAgg(sel.SelectExprs) {
32 | return nil, errors.New("'GROUP BY' & aggregation are not supported")
33 | }
34 |
35 | // Set LIMIT
36 | if sel.Limit != nil {
37 | fields = append(fields, [2]string{"limit", sqlparser.String(sel.Limit.Rowcount)})
38 | }
39 |
40 | return fields, nil
41 | }
42 |
--------------------------------------------------------------------------------
/plugins/src/pastelyzer/pastelyzer_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted [][2]string
21 | }{
22 | {`SELECT * WHERE ip='10.10.10.10'`, [][2]string{[2]string{"ip", "10.10.10.10"}}},
23 | {`select * where name='sarah' and age=40 limit 0,1`, [][2]string{[2]string{"name", "sarah"}, [2]string{"age", "40"}, [2]string{"limit", "1"}}},
24 | }
25 |
26 | for _, table := range tables {
27 | // Executed by the main service
28 | ast, err := sqlparser.Parse(table.sql)
29 | if err != nil {
30 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
31 | continue
32 | }
33 |
34 | stmt, ok := ast.(*sqlparser.Select)
35 | if !ok {
36 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
37 | continue
38 | }
39 |
40 | // Executed by the plugin
41 | result, err := c.convert(stmt)
42 | if err != nil {
43 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
44 | continue
45 | }
46 |
47 | if !equal(result, table.converted) {
48 | t.Errorf("Invalid conversion of \"%s\": \"%s\", expected: \"%s\"", table.sql, result, table.converted)
49 | }
50 | }
51 | }
52 |
53 | /*
54 | * Check whether a and b slices contain the same elements
55 | */
56 | func equal(a, b [][2]string) bool {
57 | if len(a) != len(b) {
58 | return false
59 | }
60 | for i, v := range a {
61 | if v != b[i] {
62 | return false
63 | }
64 | }
65 | return true
66 | }
67 |
--------------------------------------------------------------------------------
/plugins/src/pastelyzer/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Export symbols
9 | */
10 | var (
11 | Name = "pastelyzer"
12 | Version = "1.0.4"
13 | Plugin plugin
14 | )
15 |
16 | /*
17 | * Structure to be imported by the core as a plugin
18 | */
19 | type plugin struct {
20 |
21 | // Inherit default configuration fields
22 | source *pdk.Source
23 |
24 | // Custom fields
25 | url string
26 | limit int
27 | }
28 |
--------------------------------------------------------------------------------
/plugins/src/phishtank/README.md:
--------------------------------------------------------------------------------
1 | # Phishtank plugin
2 |
3 | Connector sends a GET request to the `phishtank.org` API and expects an XML response back.
4 | Request can contain `url` field only with a URL to check.
5 |
6 | `curl` to test:
7 | ```sh
8 | curl 'https://localhost:443/api?uuid=auth-key&sql=FROM+service+WHERE+url=%27http%3A%2F%2Fexample.com%27'
9 | ```
10 |
11 | Compile with:
12 | ```sh
13 | go build -buildmode=plugin -ldflags="-w" -o phishtank.so ./*.go
14 | ```
15 |
16 | # Limitations
17 |
18 | Does not support complex SQL queries and datetime range selection.
19 |
20 |
21 | # Access details
22 |
23 | Source YAML definition's `access` fields:
24 | - **url**: API access point, for example - `https://checkurl.phishtank.com/checkurl/index.php`
25 | - **agent**: User-Agent to use
26 |
27 |
28 | # Definition file example
29 |
30 | ```yaml
31 | name: phishtank
32 | label: Phishtank
33 | icon: database
34 |
35 | plugin: phishtank
36 | inGlobal: true
37 | includeDatetime: false
38 | supportsSQL: false
39 |
40 | access:
41 | url: https://checkurl.phishtank.com/checkurl/index.php
42 | agent: phishtank/graphoscope
43 |
44 | queryFields:
45 | - url
46 | - domain
47 |
48 |
49 | relations:
50 | -
51 | from:
52 | id: url
53 | group: url
54 | search: url
55 | attributes: ["in_database", "phish_id", "phish_detail_page", "verified", "verified_at", "valid"]
56 |
57 | to:
58 | id: domain
59 | group: domain
60 | search: domain
61 | ```
--------------------------------------------------------------------------------
/plugins/src/phishtank/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to the field/value list convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "net/url"
9 | "strings"
10 |
11 | "github.com/blastrain/vitess-sqlparser/sqlparser"
12 | )
13 |
14 | /*
15 | * Convert SQL query to the list of [field,value]
16 | */
17 | func (p *plugin) convert(sel *sqlparser.Select) ([][2]string, error) {
18 |
19 | // Handle WHERE.
20 | // Top level node pass in an empty interface
21 | // to tell the children this is root.
22 | // Is there any better way?
23 | var rootParent sqlparser.Expr
24 |
25 | // Requested field & value
26 | field, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | var fields [][2]string
32 |
33 | urlParsed, err := url.Parse(field[1])
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | // Phishtank may contain "/" at the end of a domain and may not contain,
39 | // but it is very important for the search, so we include both variants
40 | if field[0] == "url" {
41 | fields = append(fields, [2]string{field[0], field[1]})
42 |
43 | if urlParsed.Path == "" {
44 | fields = append(fields, [2]string{field[0], field[1] + "/"})
45 | } else if urlParsed.Path == "/" {
46 | fields = append(fields, [2]string{field[0], strings.TrimRight(field[1], "/")})
47 | }
48 | }
49 |
50 | // Allow to search for a domain from Web GUI
51 | if field[0] == "domain" {
52 | fields = append(fields, [2]string{"url", "https://" + field[1] + "/"})
53 | fields = append(fields, [2]string{"url", "https://" + field[1]})
54 | fields = append(fields, [2]string{"url", "http://" + field[1] + "/"})
55 | fields = append(fields, [2]string{"url", "http://" + field[1]})
56 | }
57 |
58 | return fields, nil
59 | }
60 |
--------------------------------------------------------------------------------
/plugins/src/phishtank/phishtank_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted [][2]string
21 | }{
22 | {`SELECT * WHERE url='http://example.com'`, [][2]string{
23 | [2]string{"url", "http://example.com"},
24 | [2]string{"url", "http://example.com/"},
25 | }},
26 | {`SELECT * WHERE domain='example.com'`, [][2]string{
27 | [2]string{"url", "https://example.com/"},
28 | [2]string{"url", "https://example.com"},
29 | [2]string{"url", "http://example.com/"},
30 | [2]string{"url", "http://example.com"},
31 | }},
32 | }
33 |
34 | for _, table := range tables {
35 | // Executed by the main service
36 | ast, err := sqlparser.Parse(table.sql)
37 | if err != nil {
38 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
39 | continue
40 | }
41 |
42 | stmt, ok := ast.(*sqlparser.Select)
43 | if !ok {
44 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
45 | continue
46 | }
47 |
48 | // Executed by the plugin
49 | result, err := c.convert(stmt)
50 | if err != nil {
51 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
52 | continue
53 | }
54 |
55 | if !equal(result, table.converted) {
56 | t.Errorf("Invalid conversion of '%s': %v, expected: %v", table.sql, result, table.converted)
57 | }
58 | }
59 | }
60 |
61 | /*
62 | * Check whether a and b slices contain the same elements
63 | */
64 | func equal(a, b [][2]string) bool {
65 | if len(a) != len(b) {
66 | return false
67 | }
68 | for i, v := range a {
69 | if v != b[i] {
70 | return false
71 | }
72 | }
73 | return true
74 | }
75 |
--------------------------------------------------------------------------------
/plugins/src/phishtank/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Export symbols
9 | */
10 | var (
11 | Name = "phishtank"
12 | Version = "1.0.1"
13 | Plugin plugin
14 | )
15 |
16 | /*
17 | * Structure to be imported by the core as a plugin
18 | */
19 | type plugin struct {
20 |
21 | // Inherit default configuration fields
22 | source *pdk.Source
23 |
24 | // Custom fields
25 | url string
26 | agent string
27 | limit int
28 | }
29 |
--------------------------------------------------------------------------------
/plugins/src/postgresql/README.md:
--------------------------------------------------------------------------------
1 | # PostgreSQL plugin
2 |
3 | Plugin to query PostgreSQL (https://www.postgresql.org/) as a data source.
4 |
5 |
6 | Compile with:
7 | ```sh
8 | go build -buildmode=plugin -ldflags="-w" -o postgresql.so ./*.go
9 | ```
10 |
11 | **Warning**
12 |
13 | SQL doesn't allow to query missing columns, like Elasticsearch does.
14 | An error `column "X" does not exist` will be received.
15 | That means you must be very careful with designing a data source
16 | and creating a YAML config file to be able to
17 | combine it with data source types other than SQL.
18 |
19 | The easiest solution is to exclude PostgreSQL DB from the `global` namespace
20 | and query it independently, to make sure all columns exist.
21 |
22 |
23 | # Access details
24 |
25 | Source YAML definition's `access` fields:
26 | - **addr**: HOST:PORT database's access point, for example - `localhost:5432`
27 | - **user**: username to connect to the database
28 | - **password**: user's password
29 | - **db**: database name to use
30 | - **table**: table name to query
31 |
32 |
33 | # Demo
34 |
35 | Simple example of a new PostgreSQL data source:
36 | ```sql
37 | sudo -u postgres psql
38 |
39 | CREATE DATABASE pgdb;
40 | CREATE USER graphoscope WITH ENCRYPTED PASSWORD 'password';
41 | GRANT ALL PRIVILEGES ON DATABASE pgdb TO graphoscope;
42 | \connect pgdb
43 | CREATE TABLE pgcoll (id SERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL, username VARCHAR(255) NOT NULL, fqdn VARCHAR(255) NOT NULL, count integer NOT NULL, seen TIMESTAMP);
44 | GRANT ALL PRIVILEGES ON TABLE pgcoll TO graphoscope;
45 |
46 | INSERT INTO pgcoll (email, username, fqdn, count, seen) VALUES ('a@example.com', 'a', 'example.com', 13, now());
47 | INSERT INTO pgcoll (email, username, fqdn, count, seen) VALUES ('b@example.com', 'b', 'example.com', 13, now());
48 | INSERT INTO pgcoll (email, username, fqdn, count, seen) VALUES ('c@example.com', 'c', 'example.com', 13, now());
49 | INSERT INTO pgcoll (email, username, fqdn, count, seen) VALUES ('d@example.com', 'd', 'example.com', 13, now());
50 | INSERT INTO pgcoll (email, username, fqdn, count, seen) VALUES ('e@example.com', 'e', 'example.com', 13, now());
51 | ```
52 |
53 | Access data will be used by the YAML configs. Example:
54 | ```yaml
55 | name: pgtest
56 | label: PGTest
57 | icon: database
58 |
59 | plugin: postgresql
60 | inGlobal: true
61 | includeDatetime: false
62 | supportsSQL: true
63 |
64 | access:
65 | addr: 127.0.0.1:5432
66 | db: pgdb
67 | table: pgcoll
68 | user: graphoscope
69 | password: password
70 |
71 | statsFields:
72 | - domain
73 |
74 | replaceFields:
75 | datetime: seen
76 | domain: fqdn
77 |
78 |
79 | relations:
80 | -
81 | from:
82 | id: email
83 | group: email
84 | search: email
85 | attributes: [ "username", "fqdn" ]
86 |
87 | to:
88 | id: fqdn
89 | group: domain
90 | search: domain
91 |
92 | edge:
93 | attributes: [ "count" ]
94 | ```
95 |
96 | Test with a query:
97 | ```sh
98 | curl -XGET 'https://localhost:443/api?uuid=auth-key&sql=FROM+pgtest+WHERE+email+like+%27a%25%27'
99 | ```
100 |
101 | # TODO
102 |
103 | - [ ] Check `TODO` in `convert.go`
104 |
--------------------------------------------------------------------------------
/plugins/src/postgresql/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to PostgreSQL query convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "github.com/blastrain/vitess-sqlparser/sqlparser"
9 | )
10 |
11 | /*
12 | * Convert SQL statement to the PostgreSQL filter & options
13 | */
14 | func (p *plugin) convert(sel *sqlparser.Select) (string, error) {
15 |
16 | // Handle WHERE
17 | query := sqlparser.String(sel.Where.Expr)
18 |
19 | // Handle GROUP BY
20 | if len(sel.GroupBy) > 0 {
21 | query += sqlparser.String(sel.GroupBy)
22 | }
23 |
24 | // Handle ORDER BY
25 | if sel.OrderBy != nil {
26 | query += sqlparser.String(sel.OrderBy)
27 | }
28 |
29 | // Handle LIMIT
30 | if sel.Limit != nil {
31 | // Handle offset
32 | if sel.Limit.Offset != nil {
33 | query += " OFFSET " + sqlparser.String(sel.Limit.Offset)
34 | } else {
35 | query += " OFFSET 0"
36 | }
37 |
38 | // Handle rowcount
39 | query += " LIMIT " + sqlparser.String(sel.Limit.Rowcount)
40 | }
41 |
42 | // Replace ` chars around keywords as PostgreSQL requires.
43 | //
44 | // TODO: Find cases when SQL parser produces backtiks
45 | // and replace them in a correct way. Simple "strings.Replace" will replace
46 | // expected and valid chars in the middle of the string too.
47 | //
48 | //query = strings.Replace(query, "`", "\"", -1)
49 |
50 | return query, nil
51 | }
52 |
--------------------------------------------------------------------------------
/plugins/src/postgresql/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | "github.com/jackc/pgx/v4/pgxpool"
6 | )
7 |
8 | /*
9 | * Export symbols
10 | */
11 | var (
12 | Name = "postgresql"
13 | Version = "1.0.5"
14 | Plugin plugin
15 | )
16 |
17 | /*
18 | * Structure to be imported by the core as a plugin
19 | */
20 | type plugin struct {
21 |
22 | // Inherit default configuration fields
23 | source *pdk.Source
24 |
25 | // Custom fields
26 | connection *pgxpool.Pool
27 | limit int
28 | }
29 |
--------------------------------------------------------------------------------
/plugins/src/postgresql/postgresql_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted string
21 | }{
22 | {`SELECT * WHERE ip='10.10.10.10'`, `ip = '10.10.10.10'`},
23 | {`SELECT * WHERE ip='10.10.10.10' LIMIT 5,10`, `ip = '10.10.10.10' OFFSET 5 LIMIT 10`},
24 | {`SELECT * WHERE size>100 ORDER BY name LIMIT 0,1`, `size > 100 order by name asc OFFSET 0 LIMIT 1`},
25 | {`SELECT * WHERE size=10 ORDER BY name DESC LIMIT 0,1`, `size = 10 order by name desc OFFSET 0 LIMIT 1`},
26 | {`SELECT * WHERE size>=100`, `size >= 100`},
27 | {`SELECT * WHERE name LIKE 's%'`, `name like 's%'`},
28 | {`SELECT * WHERE name NOT LIKE 's%'`, `name not like 's%'`},
29 | {`SELECT * WHERE size BETWEEN 100 AND 300`, `size between 100 and 300`},
30 | {`SELECT * WHERE size IN (100,300)`, `size in (100, 300)`},
31 | {`SELECT * WHERE size NOT IN (100,300)`, `size not in (100, 300)`},
32 | {`SELECT * WHERE name='sarah' and age!=40 AND (country='LV' OR country='AU') ORDER BY age DESC limit 1`,
33 | `name = 'sarah' and age != 40 and (country = 'LV' or country = 'AU') order by age desc OFFSET 0 LIMIT 1`},
34 | }
35 |
36 | for _, table := range tables {
37 | // Executed by the main service
38 | ast, err := sqlparser.Parse(table.sql)
39 | if err != nil {
40 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
41 | continue
42 | }
43 |
44 | stmt, ok := ast.(*sqlparser.Select)
45 | if !ok {
46 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
47 | continue
48 | }
49 |
50 | // Executed by the plugin
51 | result, err := c.convert(stmt)
52 | if err != nil {
53 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
54 | continue
55 | }
56 |
57 | if result != table.converted {
58 | t.Errorf("Invalid conversion of \"%s\": \"%s\", expected: \"%s\"", table.sql, result, table.converted)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/plugins/src/redis/README.md:
--------------------------------------------------------------------------------
1 | # Redis plugin
2 |
3 | Plugin to query Redis (https://redis.io) as a data source.
4 |
5 |
6 | Compile with:
7 | ```sh
8 | go build -buildmode=plugin -ldflags="-w" -o redis.so ./*.go
9 | ```
10 |
11 | **Warning**
12 |
13 | Redis does NOT accept complex queries, like SQL databases do.
14 |
15 | The easiest workaround is to exclude Redis DB from the `global` namespace
16 | and query it independently, to execute needed queries one by one.
17 |
18 |
19 | # Limitations
20 |
21 | Does not support complex SQL queries and datetime range selection.
22 |
23 |
24 | # Access details
25 |
26 | Source YAML definition's `access` fields:
27 | - **addr**: HOST:PORT database's access point, for example - `localhost:6379`
28 | - **user**: username to connect to the database
29 | - **password**: user's password
30 | - **db**: database number to use
31 | - **field**: Redis key will be used as this field name
32 |
33 |
34 | # Usage
35 |
36 | Simple example of a new Redis data source. Insert test data:
37 | ```sh
38 | redis-cli -u redis://localhost:6379/8
39 | ACL SETUSER graphoscope on >password allkeys +hset +hget +hgetall +select +ping
40 | ACL SAVE # Or 'CONFIG REWRITE'
41 | AUTH graphoscope password
42 |
43 | HSET 'a@example.com' username 'a' fqdn 'example.com' count 13 seen '18-02-2023T15:34:00.000000Z'
44 | HSET 'b@example.com' username 'b' fqdn 'example.com' count 13 seen '19-02-2023T15:34:00.000000Z'
45 | HSET 'c@example.com' username 'c' fqdn 'example.com' count 13 seen '20-02-2023T15:34:00.000000Z'
46 | HSET 'd@example.com' username 'd' fqdn 'example.com' count 13 seen '21-02-2023T15:34:00.000000Z'
47 | HSET 'e@example.com' username 'e' fqdn 'example.com' count 13 seen '22-02-2023T15:34:00.000000Z'
48 | ```
49 |
50 | Access data will be used by the source's YAML definition. Example:
51 | ```yaml
52 | name: retest
53 | label: RETest
54 | icon: database
55 |
56 | plugin: redis
57 | inGlobal: false
58 | includeDatetime: false
59 | supportsSQL: false
60 |
61 | access:
62 | addr: 127.0.0.1:6379
63 | user: graphoscope
64 | password: password
65 | db: 8
66 | field: email
67 |
68 | queryFields:
69 | - email
70 |
71 | replaceFields:
72 | datetime: seen
73 | domain: fqdn
74 |
75 |
76 | relations:
77 | -
78 | from:
79 | id: email
80 | group: email
81 | search: email
82 | attributes: [ "seen", "fqdn" ]
83 |
84 | to:
85 | id: username
86 | group: username
87 | search: username
88 |
89 | edge:
90 | attributes: [ "count" ]
91 | ```
92 |
93 | Test with a query:
94 | ```sh
95 | curl -XGET 'https://localhost:443/api?uuid=auth-key&sql=FROM+retest+WHERE+email=%27a@example.com%27'
96 | ```
97 |
--------------------------------------------------------------------------------
/plugins/src/redis/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to the field/value list convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "github.com/blastrain/vitess-sqlparser/sqlparser"
9 | )
10 |
11 | /*
12 | * Convert SQL query to the list of [field,value]
13 | */
14 | func (p *plugin) convert(sel *sqlparser.Select) (string, error) {
15 |
16 | // Handle WHERE.
17 | // Top level node pass in an empty interface
18 | // to tell the children this is root.
19 | // Is there any better way?
20 | var rootParent sqlparser.Expr
21 |
22 | // List of requested fields & values
23 | fields, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
24 | if err != nil {
25 | return "", err
26 | }
27 |
28 | if len(fields) == 1 && fields[0][0] == p.source.Access["field"] {
29 | return fields[0][1], nil
30 | }
31 |
32 | return "", nil
33 | }
34 |
--------------------------------------------------------------------------------
/plugins/src/redis/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | "github.com/redis/go-redis/v9"
6 | )
7 |
8 | /*
9 | * Export symbols
10 | */
11 | var (
12 | Name = "redis"
13 | Version = "1.0.0"
14 | Plugin plugin
15 | )
16 |
17 | /*
18 | * Structure to be imported by the core as a plugin
19 | */
20 | type plugin struct {
21 |
22 | // Inherit default configuration fields
23 | source *pdk.Source
24 |
25 | // Custom fields
26 | client *redis.Client
27 | limit int
28 | }
29 |
--------------------------------------------------------------------------------
/plugins/src/redis/redis_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "testing"
6 |
7 | "github.com/blastrain/vitess-sqlparser/sqlparser"
8 | "github.com/cert-lv/graphoscope/pdk"
9 | )
10 |
11 | /*
12 | * Test SQL conversion to the data source's expected format
13 | */
14 | func TestConvert(t *testing.T) {
15 |
16 | // Empty plugin's instance to test
17 | p := plugin{}
18 |
19 | p.source = &pdk.Source{
20 | Access: map[string]string{
21 | "field": "email",
22 | },
23 | }
24 |
25 | // Pairs of SQLs and the expected results
26 | tables := []struct {
27 | sql string
28 | converted string
29 | }{
30 | {`SELECT * WHERE email='a@example.com'`, "a@example.com"},
31 | {`SELECT * WHERE user='a@example.com'`, ""},
32 | }
33 |
34 | for _, table := range tables {
35 | // Executed by the main service
36 | ast, err := sqlparser.Parse(table.sql)
37 | if err != nil {
38 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
39 | continue
40 | }
41 |
42 | stmt, ok := ast.(*sqlparser.Select)
43 | if !ok {
44 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
45 | continue
46 | }
47 |
48 | // Executed by the plugin
49 | fmt.Println(1, p.source)
50 | result, err := p.convert(stmt)
51 | if err != nil {
52 | t.Errorf("Can't convert \"%s\": %s", table.sql, err.Error())
53 | continue
54 | }
55 |
56 | if result != table.converted {
57 | t.Errorf("Invalid conversion of \"%s\": %v, expected: %v", table.sql, result, table.converted)
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/plugins/src/rest/README.md:
--------------------------------------------------------------------------------
1 | # REST API plugin
2 |
3 | Connector sends a GET request and expects a list of flat JSON objects back, one line - one JSON.
4 | To the preconfigured REST API URL `field/value` will be attached as query.
5 |
6 | `curl` to test:
7 | ```sh
8 | curl 'https://localhost:443/api?uuid=auth-key&sql=FROM+service+WHERE+ip=%278.8.8.8%27'
9 | ```
10 |
11 | Compile with:
12 | ```sh
13 | go build -buildmode=plugin -ldflags="-w" -o rest.so ./*.go
14 | ```
15 |
16 | # Access details
17 |
18 | Source YAML definition's `access` fields:
19 | - **url**: REST API access point, for example - `http://localhost:8000/RESTv1`
20 | - **username**: Username if exists
21 | - **password**: User's password if exists
22 |
--------------------------------------------------------------------------------
/plugins/src/rest/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to the field/value list convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "github.com/blastrain/vitess-sqlparser/sqlparser"
9 | )
10 |
11 | /*
12 | * Convert SQL query to the list of [field,value]
13 | */
14 | func (p *plugin) convert(sel *sqlparser.Select) ([2]string, error) {
15 |
16 | // Handle WHERE.
17 | // Top level node pass in an empty interface
18 | // to tell the children this is root.
19 | // Is there any better way?
20 | var rootParent sqlparser.Expr
21 |
22 | // List of requested fields & values
23 | fields, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
24 | if err != nil {
25 | return [2]string{}, err
26 | }
27 |
28 | return fields, nil
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/src/rest/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Export symbols
9 | */
10 | var (
11 | Name = "rest"
12 | Version = "1.0.1"
13 | Plugin plugin
14 | )
15 |
16 | /*
17 | * Structure to be imported by the core as a plugin
18 | */
19 | type plugin struct {
20 |
21 | // Inherit default configuration fields
22 | source *pdk.Source
23 |
24 | // Custom fields
25 | url string
26 | username string
27 | password string
28 | limit int
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/src/rest/rest_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted [2]string
21 | }{
22 | {`SELECT * WHERE ip='10.10.10.10' LIMIT 5,1`, [2]string{"ip", "10.10.10.10"}},
23 | {`select * where name='sarah'`, [2]string{"name", "sarah"}},
24 | }
25 |
26 | for _, table := range tables {
27 | // Executed by the main service
28 | ast, err := sqlparser.Parse(table.sql)
29 | if err != nil {
30 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
31 | continue
32 | }
33 |
34 | stmt, ok := ast.(*sqlparser.Select)
35 | if !ok {
36 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
37 | continue
38 | }
39 |
40 | // Executed by the plugin
41 | result, err := c.convert(stmt)
42 | if err != nil {
43 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
44 | continue
45 | }
46 |
47 | if !equal(result, table.converted) {
48 | t.Errorf("Invalid conversion of '%s': %v, expected: %v", table.sql, result, table.converted)
49 | }
50 | }
51 | }
52 |
53 | /*
54 | * Check whether a and b slices contain the same elements
55 | */
56 | func equal(a, b [2]string) bool {
57 | for i, v := range a {
58 | if v != b[i] {
59 | return false
60 | }
61 | }
62 | return true
63 | }
64 |
--------------------------------------------------------------------------------
/plugins/src/shodan/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to the field/value list convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "github.com/blastrain/vitess-sqlparser/sqlparser"
9 | )
10 |
11 | /*
12 | * Convert SQL query to the list of [field,value]
13 | */
14 | func (p *plugin) convert(sel *sqlparser.Select) ([2]string, error) {
15 |
16 | // Handle WHERE.
17 | // Top level node pass in an empty interface
18 | // to tell the children this is root.
19 | // Is there any better way?
20 | var rootParent sqlparser.Expr
21 |
22 | // Requested field & value
23 | field, err := handleSelectWhere(&sel.Where.Expr, true, &rootParent)
24 | if err != nil {
25 | return [2]string{}, err
26 | }
27 |
28 | return field, nil
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/src/shodan/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Export symbols
9 | */
10 | var (
11 | Name = "shodan"
12 | Version = "1.0.1"
13 | Plugin plugin
14 | )
15 |
16 | /*
17 | * Structure to be imported by the core as a plugin
18 | */
19 | type plugin struct {
20 |
21 | // Inherit default configuration fields
22 | source *pdk.Source
23 |
24 | // Custom fields
25 | limit int
26 | pages int
27 | credits int
28 | }
29 |
--------------------------------------------------------------------------------
/plugins/src/shodan/shodan_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted [2]string
21 | }{
22 | {`SELECT * WHERE ip='8.8.8.8'`, [2]string{"ip", "8.8.8.8"}},
23 | }
24 |
25 | for _, table := range tables {
26 | // Executed by the main service
27 | ast, err := sqlparser.Parse(table.sql)
28 | if err != nil {
29 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
30 | continue
31 | }
32 |
33 | stmt, ok := ast.(*sqlparser.Select)
34 | if !ok {
35 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
36 | continue
37 | }
38 |
39 | // Executed by the plugin
40 | result, err := c.convert(stmt)
41 | if err != nil {
42 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
43 | continue
44 | }
45 |
46 | if !equal(result, table.converted) {
47 | t.Errorf("Invalid conversion of '%s': %v, expected: %v", table.sql, result, table.converted)
48 | }
49 | }
50 | }
51 |
52 | /*
53 | * Check whether a and b slices contain the same elements
54 | */
55 | func equal(a, b [2]string) bool {
56 | if a[0] != b[0] || a[1] != b[1] {
57 | return false
58 | }
59 |
60 | return true
61 | }
62 |
--------------------------------------------------------------------------------
/plugins/src/sqlite/README.md:
--------------------------------------------------------------------------------
1 | # SQLite plugin
2 |
3 | Plugin to query SQLite (https://www.sqlite.org) as a data source.
4 |
5 |
6 | Compile with:
7 | ```sh
8 | CGO_EBABLED=1 CGO_CFLAGS="-g -O2 -Wno-return-local-addr" go build -buildmode=plugin -ldflags="-w" -o sqlite.so ./*.go
9 | ```
10 | Test with:
11 | ```sh
12 | CGO_CFLAGS="-g -O2 -Wno-return-local-addr" go test
13 | ```
14 | **CFLAGS** as a temp. solution for the https://github.com/mattn/go-sqlite3/issues/803
15 |
16 |
17 | **Warnings**
18 |
19 | When a `SIGSEGV` occurs while running a C code called via cgo (what SQLite
20 | plugin does), that `SIGSEGV` is not turned into a Go panic. The mechanism that
21 | Go uses to turn a memory error into a panic can only work for a Go code, not
22 | for a C code. That means `segmentation violation` errors in a C code will crash
23 | the API service.
24 |
25 | ---
26 |
27 | SQL doesn't allow to query missing columns, like Elasticsearch does.
28 | An error `no such column: X` will be received. That means you must be very
29 | careful with designing a data source and creating a YAML config file to be able
30 | to combine it with data source types other than SQL.
31 |
32 | The easiest solution is to exclude SQLite DB from the `global` namespace and
33 | query it independently, to make sure all columns exist.
34 |
35 |
36 | # Access details
37 |
38 | Source YAML definition's `access` fields:
39 | - **db**: database file to use, for example - `/data/sqtest.db`
40 | - **table**: table name to query
41 |
42 |
43 | # Demo
44 |
45 | Simple example of creation a new SQLite data source from a CLI:
46 | ```sql
47 | sqlite3 sqtest.db
48 |
49 | CREATE TABLE sqcoll (email VARCHAR(255) NOT NULL, username VARCHAR(255) NOT NULL, fqdn VARCHAR(255) NOT NULL, count integer NOT NULL, seen TIMESTAMP);
50 | INSERT INTO sqcoll (email, username, fqdn, count, seen) VALUES ('a@example.com', 'a', 'example.com', 13, DateTime('now', 'localtime'));
51 | INSERT INTO sqcoll (email, username, fqdn, count, seen) VALUES ('b@example.com', 'b', 'example.com', 13, DateTime('now', 'localtime'));
52 | INSERT INTO sqcoll (email, username, fqdn, count, seen) VALUES ('c@example.com', 'c', 'example.com', 13, DateTime('now', 'localtime'));
53 | INSERT INTO sqcoll (email, username, fqdn, count, seen) VALUES ('d@example.com', 'd', 'example.com', 13, DateTime('now', 'localtime'));
54 | INSERT INTO sqcoll (email, username, fqdn, count, seen) VALUES ('e@example.com', 'e', 'example.com', 13, DateTime('now', 'localtime'));
55 | .quit
56 | ```
57 |
58 | Access data will be used by the YAML configs. Example:
59 | ```yaml
60 | name: sqtest
61 | label: SQTest
62 | icon: database
63 |
64 | plugin: sqlite
65 | inGlobal: true
66 | includeDatetime: false
67 | supportsSQL: true
68 |
69 | access:
70 | db: /data/sqtest.db
71 | table: sqcoll
72 |
73 | statsFields:
74 | - domain
75 |
76 | replaceFields:
77 | datetime: seen
78 | domain: fqdn
79 |
80 |
81 | relations:
82 | -
83 | from:
84 | id: email
85 | group: email
86 | search: email
87 | attributes: [ "username", "fqdn" ]
88 |
89 | to:
90 | id: fqdn
91 | group: domain
92 | search: domain
93 |
94 | edge:
95 | attributes: [ "count" ]
96 | ```
97 |
98 | Test with a query:
99 | ```sh
100 | curl -XGET 'https://localhost:443/api?uuid=auth-key&sql=FROM+sqtest+WHERE+email+like+%27a%25%27'
101 | ```
102 |
--------------------------------------------------------------------------------
/plugins/src/sqlite/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to SQLite query convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "github.com/blastrain/vitess-sqlparser/sqlparser"
9 | )
10 |
11 | /*
12 | * Convert SQL statement to the SQLite query
13 | */
14 | func (p *plugin) convert(sel *sqlparser.Select) (string, error) {
15 |
16 | // Handle WHERE
17 | query := sqlparser.String(sel.Where.Expr)
18 |
19 | // Handle GROUP BY
20 | if len(sel.GroupBy) > 0 {
21 | query += sqlparser.String(sel.GroupBy)
22 | }
23 |
24 | // Handle ORDER BY
25 | if sel.OrderBy != nil {
26 | query += sqlparser.String(sel.OrderBy)
27 | }
28 |
29 | // Handle LIMIT offset,rowcount
30 | if sel.Limit != nil {
31 | if sel.Limit.Offset != nil {
32 | query += " LIMIT " + sqlparser.String(sel.Limit.Offset) + "," + sqlparser.String(sel.Limit.Rowcount)
33 | } else {
34 | query += " LIMIT 0," + sqlparser.String(sel.Limit.Rowcount)
35 | }
36 | }
37 |
38 | return query, nil
39 | }
40 |
--------------------------------------------------------------------------------
/plugins/src/sqlite/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 |
6 | "github.com/cert-lv/graphoscope/pdk"
7 | )
8 |
9 | /*
10 | * Export symbols
11 | */
12 | var (
13 | Name = "sqlite"
14 | Version = "1.0.5"
15 | Plugin plugin
16 | )
17 |
18 | /*
19 | * Structure to be imported by the core as a plugin
20 | */
21 | type plugin struct {
22 |
23 | // Inherit default configuration fields
24 | source *pdk.Source
25 |
26 | // Custom fields
27 | db *sql.DB
28 | limit int
29 | }
30 |
--------------------------------------------------------------------------------
/plugins/src/sqlite/sqlite_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * Test SQL conversion to the data source's expected format
11 | */
12 | func TestConvert(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := plugin{}
16 |
17 | // Pairs of SQLs and the expected results
18 | tables := []struct {
19 | sql string
20 | converted string
21 | }{
22 | {`SELECT * WHERE ip='10.10.10.10'`, `ip = '10.10.10.10'`},
23 | {`SELECT * WHERE ip='10.10.10.10' LIMIT 5,10`, `ip = '10.10.10.10' LIMIT 5,10`},
24 | {`SELECT * WHERE size>100 ORDER BY name LIMIT 0,1`, `size > 100 order by name asc LIMIT 0,1`},
25 | {`SELECT * WHERE size=10 ORDER BY name DESC LIMIT 0,1`, `size = 10 order by name desc LIMIT 0,1`},
26 | {`SELECT * WHERE size>=100`, `size >= 100`},
27 | {`SELECT * WHERE name LIKE 's%'`, `name like 's%'`},
28 | {`SELECT * WHERE name NOT LIKE 's%'`, `name not like 's%'`},
29 | {`SELECT * WHERE size BETWEEN 100 AND 300`, `size between 100 and 300`},
30 | {`SELECT * WHERE size IN (100,300)`, `size in (100, 300)`},
31 | {`SELECT * WHERE size NOT IN (100,300)`, `size not in (100, 300)`},
32 | {`SELECT * WHERE name='sarah' and age!=40 AND (country='LV' OR country='AU') ORDER BY age DESC limit 1`,
33 | `name = 'sarah' and age != 40 and (country = 'LV' or country = 'AU') order by age desc LIMIT 0,1`},
34 | }
35 |
36 | for _, table := range tables {
37 | // Executed by the main service
38 | ast, err := sqlparser.Parse(table.sql)
39 | if err != nil {
40 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
41 | continue
42 | }
43 |
44 | stmt, ok := ast.(*sqlparser.Select)
45 | if !ok {
46 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
47 | continue
48 | }
49 |
50 | // Executed by the plugin
51 | result, err := c.convert(stmt)
52 | if err != nil {
53 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
54 | continue
55 | }
56 |
57 | if result != table.converted {
58 | t.Errorf("Invalid conversion of \"%s\": \"%s\", expected: \"%s\"", table.sql, result, table.converted)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/plugins/src/taxonomy/README.md:
--------------------------------------------------------------------------------
1 | # Taxonomy plugin
2 |
3 | Apply the predefined taxonomy to all nodes. New virtual nodes will be inserted, which will group existing nodes.
4 |
5 |
6 | Compile with:
7 | ```sh
8 | go build -buildmode=plugin -ldflags="-w" -o taxonomy.so ./*.go
9 | ```
10 |
11 | # Configuration
12 |
13 | YAML definition's `data` fields:
14 | - **field**: field to use as a filter
15 | - **group**: as all graph nodes belong to some group/type, it can be used to apply taxonomy to the specific group only
16 | - **taxonomy**: mapping to use. If node/edge's **field** is equal to the "key" - insert a new relation with "value" as a new node
17 |
18 | Mapping example:
19 | ```yaml
20 | taxonomy:
21 | field_value_1: taxonomy_group
22 | field_value_2: taxonomy_group
23 | ```
24 |
--------------------------------------------------------------------------------
/plugins/src/taxonomy/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Export symbols
9 | */
10 | var (
11 | Name = "taxonomy"
12 | Version = "1.0.0"
13 | Plugin plugin
14 | )
15 |
16 | /*
17 | * Structure to be imported by the core as a plugin
18 | */
19 | type plugin struct {
20 |
21 | // Inherit default configuration fields
22 | processor *pdk.Processor
23 | }
24 |
--------------------------------------------------------------------------------
/plugins/src/taxonomy/taxonomy.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Check "pdk/plugin.go" for the built-in plugin functions description
9 | */
10 |
11 | func (p *plugin) Conf() *pdk.Processor {
12 | return p.processor
13 | }
14 |
15 | func (p *plugin) Setup(processor *pdk.Processor) error {
16 |
17 | // Store settings
18 | p.processor = processor
19 |
20 | return nil
21 | }
22 |
23 | func (p *plugin) Process(relations []map[string]interface{}) ([]map[string]interface{}, error) {
24 |
25 | for _, relation := range relations {
26 | for k, v := range p.processor.Data["taxonomy"].(map[string]interface{}) {
27 | for _, part := range []string{"from", "to", "edge"} {
28 | rp := relation[part]
29 |
30 | if rp != nil && (rp.(map[string]interface{})[p.processor.Data["field"].(string)] == k ||
31 | (rp.(map[string]interface{})["attributes"] != nil &&
32 | rp.(map[string]interface{})["attributes"].(map[string]interface{})[p.processor.Data["field"].(string)] == k)) {
33 |
34 | if p.processor.Data["group"] != nil &&
35 | rp.(map[string]interface{})["group"] != p.processor.Data["group"].(string) {
36 | continue
37 | }
38 |
39 | taxRelation := p.createRelation(v.(string), k,
40 | rp.(map[string]interface{})["group"].(string),
41 | rp.(map[string]interface{})["search"].(string),
42 | )
43 |
44 | relations = append(relations, taxRelation)
45 | }
46 | }
47 | }
48 | }
49 |
50 | return relations, nil
51 | }
52 |
53 | /*
54 | * Generate new graph relation to display taxonomy info as a new node
55 | */
56 | func (p *plugin) createRelation(tid, fid, group, search string) map[string]interface{} {
57 | from := map[string]interface{}{
58 | "id": fid,
59 | "group": group,
60 | "search": search,
61 | }
62 |
63 | to := map[string]interface{}{
64 | "id": tid,
65 | "group": "taxonomy",
66 | "search": "taxonomy",
67 | }
68 |
69 | // Resulting graph relation to return
70 | result := make(map[string]interface{})
71 |
72 | // Put it together
73 | result["from"] = from
74 | result["to"] = to
75 | result["source"] = p.Conf().Name
76 |
77 | return result
78 | }
79 |
80 | func (p *plugin) Stop() error {
81 | return nil
82 | }
83 |
--------------------------------------------------------------------------------
/plugins/src/taxonomy/taxonomy_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/cert-lv/graphoscope/pdk"
7 | )
8 |
9 | /*
10 | * Test taxonomy nodes creating
11 | */
12 | func TestProcess(t *testing.T) {
13 |
14 | // Empty plugin's instance to test
15 | c := &plugin{}
16 |
17 | processor := &pdk.Processor{
18 | Data: map[string]interface{}{
19 | "field": "id",
20 | "group": "type",
21 |
22 | "taxonomy": map[string]interface{}{
23 | "brute-force": "intrusion-attempts",
24 | },
25 | },
26 | }
27 |
28 | err := c.Setup(processor)
29 | if err != nil {
30 | t.Errorf("Can't setup a taxonomy plugin: %s", err.Error())
31 | }
32 |
33 | // Pairs of data source plugins responses and the expected processing results
34 | table := []struct {
35 | entry map[string]interface{}
36 | inserted map[string]interface{}
37 | }{
38 | // New relation should be added because "id" == "brute-force"
39 | {map[string]interface{}{
40 | "from": map[string]interface{}{
41 | "id": "brute-force",
42 | "group": "type",
43 | "search": "type",
44 | },
45 | }, map[string]interface{}{
46 | "id": "intrusion-attempts",
47 | "group": "taxonomy",
48 | "search": "taxonomy",
49 | }},
50 |
51 | // New relation should be added because "attributes.id" == "brute-force"
52 | {map[string]interface{}{
53 | "from": map[string]interface{}{
54 | "id": "malware",
55 | "group": "type",
56 | "search": "type",
57 | "attributes": map[string]interface{}{
58 | "id": "brute-force",
59 | },
60 | },
61 | }, map[string]interface{}{
62 | "id": "intrusion-attempts",
63 | "group": "taxonomy",
64 | "search": "taxonomy",
65 | }},
66 |
67 | // New relation should NOT be added because "id" value is not mentioned in processor.Data["taxonomy"]
68 | {map[string]interface{}{
69 | "from": map[string]interface{}{
70 | "id": "ddos",
71 | "group": "type",
72 | "search": "type",
73 | },
74 | }, nil},
75 |
76 | // New relation should NOT be added because "group" value is not equal to processor.Data["group"]
77 | {map[string]interface{}{
78 | "from": map[string]interface{}{
79 | "id": "brute-force",
80 | "group": "address",
81 | "search": "type",
82 | },
83 | }, nil},
84 | }
85 |
86 | for _, row := range table {
87 | result, err := c.Process([]map[string]interface{}{row.entry})
88 | if err != nil {
89 | t.Errorf("Can't process '%s': %s", row.entry, err.Error())
90 | continue
91 | }
92 |
93 | if row.inserted == nil && len(result) > 1 {
94 | t.Errorf("Unwanted relation added for \"%s\": \"%s\"", row.entry["from"], result[1])
95 |
96 | } else if len(result) == 1 && row.inserted != nil {
97 | t.Errorf("No new relations added for \"%s\": \"%s\", expected: \"%s\"", row.entry["from"], result, row.inserted)
98 |
99 | } else if len(result) > 1 && row.inserted != nil {
100 | for k, v := range result[1]["to"].(map[string]interface{}) {
101 | if row.inserted[k] != v {
102 | t.Errorf("Invalid taxonomy added for \"%s\": \"%s\", expected: \"%s\"",
103 | row.entry["from"], result[1]["to"], row.inserted)
104 | break
105 | }
106 | }
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/plugins/src/template/README.md:
--------------------------------------------------------------------------------
1 | ### STEP 16.
2 | ### Write plugin description and documentation
3 |
4 |
5 | # Template plugin
6 |
7 | Template to build new plugins.
8 | Check GUI built-in documentation section `Administration` for a complete
9 | step-by-step workflow.
10 |
11 |
12 | Compile with:
13 | ```sh
14 | go build -buildmode=plugin -ldflags="-w" -o template.so ./*.go
15 | ```
16 |
17 | # Access details
18 |
19 | Source YAML definition's `access` fields:
20 | - **url**: PROTO://HOST:PORT database's access point, for example - `127.0.0.1:3000`
21 | - **user**: username to connect with
22 | - **password**: user's password
23 | - **db**: database name to use
24 | - **collection**: collection name to query
25 |
--------------------------------------------------------------------------------
/plugins/src/template/convert.go:
--------------------------------------------------------------------------------
1 | /*
2 | * SQL to X query convertor
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "github.com/blastrain/vitess-sqlparser/sqlparser"
9 | )
10 |
11 | /*
12 | * Convert SQL statement to the object expected by the data source
13 | */
14 | func (p *plugin) convert(sel *sqlparser.Select) (string, error) {
15 |
16 | /*
17 | * STEP 8.
18 | *
19 | * Do the SQL conversion.
20 | * Check, for example, a MongoDB plugin to see how SQL
21 | * can be converted to the hierarchical object.
22 | *
23 | * Here we just return a simple static 'field=value' pair.
24 | *
25 | * File not needed for the processor plugin!
26 | */
27 |
28 | filter := "field=value"
29 | return filter, nil
30 | }
31 |
--------------------------------------------------------------------------------
/plugins/src/template/plugin.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/cert-lv/graphoscope/pdk"
5 | )
6 |
7 | /*
8 | * Export symbols
9 | */
10 | var (
11 | /*
12 | * STEP 15.
13 | *
14 | * Set plugin name and version
15 | */
16 |
17 | Name = "template"
18 | Version = "1.0.0"
19 | Plugin plugin
20 | )
21 |
22 | /*
23 | * Structure to be imported by the core as a plugin
24 | */
25 | type plugin struct {
26 |
27 | /*
28 | * STEP 13.
29 | *
30 | * Inherit default configuration fields for the data source or
31 | * processor plugin
32 | */
33 |
34 | source *pdk.Source
35 | //processor *pdk.Processor
36 |
37 | /*
38 | * STEP 14.
39 | *
40 | * Define all the custom fields needed by the plugin,
41 | * such as "client" object, database/collection name, etc..
42 | */
43 |
44 | //client *package.Client
45 | limit int
46 | }
47 |
--------------------------------------------------------------------------------
/plugins/src/template/template_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/blastrain/vitess-sqlparser/sqlparser"
7 | )
8 |
9 | /*
10 | * STEP 17.
11 | *
12 | * Test plugin's functionality
13 | */
14 |
15 | func TestConvert(t *testing.T) {
16 |
17 | // Empty plugin's instance to test
18 | c := plugin{}
19 |
20 | // Test whether example SQL queries are correctly converted to the expected format.
21 | // Pairs of SQLs and the expected results:
22 | tables := []struct {
23 | sql string
24 | converted string
25 | }{
26 | {`SELECT * WHERE ip='10.10.10.10' LIMIT 0,10`, ``},
27 | {`SELECT * WHERE size>100 ORDER BY name LIMIT 5,1`, ``},
28 | {`SELECT * WHERE size=10 ORDER BY name DESC LIMIT 0,1`, ``},
29 | {`SELECT * WHERE size>=100 LIMIT 0,1`, ``},
30 | {`SELECT * WHERE name LIKE 's%' LIMIT 0,1`, ``},
31 | {`SELECT * WHERE name NOT LIKE 's%' LIMIT 0,1`, ``},
32 | {`SELECT * WHERE size BETWEEN 100 AND 300 LIMIT 0,1`, ``},
33 | {`SELECT * WHERE size IN (100,300) LIMIT 0,1`, ``},
34 | {`SELECT * WHERE size NOT IN (100,300) LIMIT 0,1`, ``},
35 | {`SELECT * WHERE name='sarah' AND age!=40 AND (country='LV' OR country='AU') ORDER BY age DESC LIMIT 1`, ``},
36 | }
37 |
38 | for _, table := range tables {
39 | // Executed by the main service
40 | ast, err := sqlparser.Parse(table.sql)
41 | if err != nil {
42 | t.Errorf("Can't parse '%s': %s", table.sql, err.Error())
43 | continue
44 | }
45 |
46 | stmt, ok := ast.(*sqlparser.Select)
47 | if !ok {
48 | t.Errorf("Only SELECT statement is allowed: %s", table.sql)
49 | continue
50 | }
51 |
52 | // Executed by the plugin
53 | result, err := c.convert(stmt)
54 | if err != nil {
55 | t.Errorf("Can't convert '%s': %s", table.sql, err.Error())
56 | continue
57 | }
58 |
59 | if result != table.converted {
60 | t.Errorf("Invalid conversion of \"%s\": \"%s\", expected: \"%s\"", table.sql, result, table.converted)
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------