-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathdatabase.go
973 lines (867 loc) · 30.5 KB
/
database.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
package cmd
import (
"bufio"
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/briandowns/spinner"
"github.com/defectdojo/godojo/distros"
c "github.com/mtesauro/commandeer"
)
// Setup a struct to use for DB commands
type sqlStr struct {
os string
sql string
errMsg string
creds map[string]string
kind string
}
// saneDBConfig checks if the options configured in dojoConfig.yml are
// possible aka sane and will exist the installer with a message if they are not
func saneDBConfig(d *DDConfig) {
// Remote database that doesn't exist - godojo can't help you here
if !d.conf.Install.DB.Local && !d.conf.Install.DB.Exists {
d.errorMsg("Remote database which doens't exist was confgiured in dojoConfig.yml.")
d.errorMsg("This is an unsupported configuration.")
d.statusMsg("Correct configuration and/or install a remote DB before running installer again.")
fmt.Printf("Exiting...\n\n")
os.Exit(1)
}
}
// prepDBForDojo
func installDBForDojo(d *DDConfig, t *targetOS) {
// Handle the case that the DB is local and doesn't exist
if !d.conf.Install.DB.Exists {
// Note that godojo won't try to install remote databases
dbNotExist(d, t)
}
// Install DB clients for remote DBs
if !d.conf.Install.DB.Local {
dbClient(d, t)
}
// Start the database if local and didn't already exist
if d.conf.Install.DB.Local && !d.conf.Install.DB.Exists {
localDBStart(d, t)
}
}
// dbNotExist takes a pointer to a DDConfig struct and a pointer to targetOS
// struct and runs the commands necesary to install a local database of the
// supported type (PostgreSQL, MySQL, etc)
func dbNotExist(d *DDConfig, t *targetOS) {
// Handle the case that the DB is local and doesn't exist
d.sectionMsg("Installing database needed for DefectDojo")
// Create a new install DB command package
cInstallDB := c.NewPkg("installdb")
// Get commands for the right distro & DB
switch {
case t.distro == "ubuntu":
d.traceMsg("DB needs to be installed on Ubuntu")
err := distros.GetUbuntuDB(cInstallDB, t.id, d.conf.Install.DB.Engine)
if err != nil {
fmt.Printf("Error searching for commands to install DB on target OS %s was\n", t.id)
fmt.Printf("\t%+v\n", err)
os.Exit(1)
}
if strings.ToLower(d.conf.Install.DB.Engine) == "mysql" {
d.warnMsg("WARNING: While supported, there is significantly more testing with PostreSQL than MySQL. YMMV.")
}
case t.distro == "rhel":
d.traceMsg("DB needs to be installed on RHEL")
err := distros.GetRHELDB(cInstallDB, t.id, d.conf.Install.DB.Engine)
if err != nil {
fmt.Printf("Error searching for commands to install DB on target OS %s was\n", t.id)
fmt.Printf("\t%+v\n", err)
os.Exit(1)
}
if strings.ToLower(d.conf.Install.DB.Engine) == "mysql" {
d.warnMsg("WARNING: While supported, there is significantly more testing with PostreSQL than MySQL. YMMV.")
}
default:
d.traceMsg(fmt.Sprintf("Distro identified (%s) is not supported", t.id))
fmt.Printf("Distro identified by godojo (%s) is not supported, exiting...\n", t.id)
os.Exit(1)
}
// Run the commands to install the chosen DB
d.spin = spinner.New(spinner.CharSets[34], 100*time.Millisecond)
d.spin.Prefix = "Installing " + d.conf.Install.DB.Engine + " database for DefectDojo..."
d.spin.Start()
// Run the install DB for the target OS
tCmds, err := distros.CmdsForTarget(cInstallDB, t.id)
if err != nil {
fmt.Printf("Error getting commands to install DB target OS %s\n", t.id)
os.Exit(1)
}
for i := range tCmds {
sendCmd(d,
d.cmdLogger,
tCmds[i].Cmd,
tCmds[i].Errmsg,
tCmds[i].Hard)
}
d.spin.Stop()
d.statusMsg("Installing Database complete")
}
// dbClientInstall
func dbClient(d *DDConfig, t *targetOS) {
// Handle the case that the DB is local and doesn't exist
d.sectionMsg("Installing database client needed for DefectDojo")
// Create a new install DB client command package
cInstallDBClient := c.NewPkg("installdbclient")
// Get the commands for the right distro & DB
switch {
case t.distro == "ubuntu":
d.traceMsg("DB client needs to be installed on Ubuntu")
err := distros.GetUbuntuDB(cInstallDBClient, t.id, d.conf.Install.DB.Engine)
if err != nil {
fmt.Printf("Error searching for commands to install DB client on target OS %s was\n", t.id)
fmt.Printf("\t%+v\n", err)
os.Exit(1)
}
case t.distro == "rhel":
d.traceMsg("DB client needs to be installed on RHEL")
err := distros.GetRHELDB(cInstallDBClient, t.id, d.conf.Install.DB.Engine)
if err != nil {
fmt.Printf("Error searching for commands to install DB client on target OS %s was\n", t.id)
fmt.Printf("\t%+v\n", err)
os.Exit(1)
}
default:
d.traceMsg(fmt.Sprintf("Distro identified (%s) is not supported", t.id))
fmt.Printf("Distro identified by godojo (%s) is not supported, exiting...\n", t.id)
os.Exit(1)
}
// Run the commands to install the chosen DB
d.spin = spinner.New(spinner.CharSets[34], 100*time.Millisecond)
d.spin.Prefix = "Installing " + d.conf.Install.DB.Engine + " database client for DefectDojo..."
d.spin.Start()
// Run the install DB client for the target OS
tCmds, err := distros.CmdsForTarget(cInstallDBClient, t.id)
if err != nil {
fmt.Printf("Error getting commands to install DB target OS %s\n", t.id)
os.Exit(1)
}
for i := range tCmds {
sendCmd(d,
d.cmdLogger,
tCmds[i].Cmd,
tCmds[i].Errmsg,
tCmds[i].Hard)
}
d.spin.Stop()
d.statusMsg("Installing Database client complete")
}
// localDBStart
func localDBStart(d *DDConfig, t *targetOS) {
// Handle the case that the DB is local and doesn't exist
d.sectionMsg("Starting the database needed for DefectDojo")
// Create new boostrap command package
cStartDB := c.NewPkg("startdb")
// Get commands for the right distro
switch {
case t.distro == "ubuntu":
d.traceMsg("Searching for commands to start MySQL under Ubuntu")
err := distros.GetUbuntuDB(cStartDB, t.id, d.conf.Install.DB.Engine)
if err != nil {
fmt.Printf("Error searching for commands to start database under target OS %s\n", t.id)
os.Exit(1)
}
case t.distro == "rhel":
d.traceMsg("Searching for commands to start MySQL under RHEL")
err := distros.GetRHELDB(cStartDB, t.id, d.conf.Install.DB.Engine)
if err != nil {
fmt.Printf("Error searching for commands to start database under target OS %s\n", t.id)
os.Exit(1)
}
default:
d.traceMsg(fmt.Sprintf("Distro identified (%s) is not supported", t.id))
fmt.Printf("Distro identified by godojo (%s) is not supported, exiting...\n", t.id)
os.Exit(1)
}
// Run the commands to install the chosen DB
d.spin = spinner.New(spinner.CharSets[34], 100*time.Millisecond)
d.spin.Prefix = "Starting " + d.conf.Install.DB.Engine + " database for DefectDojo..."
d.spin.Start()
// Run the start DB command(s) for the target OS
tCmds, err := distros.CmdsForTarget(cStartDB, t.id)
if err != nil {
fmt.Printf("Error getting commands to start DB on target OS %s\n", t.id)
os.Exit(1)
}
for i := range tCmds {
sendCmd(d,
d.cmdLogger,
tCmds[i].Cmd,
tCmds[i].Errmsg,
tCmds[i].Hard)
}
d.spin.Stop()
d.statusMsg("Starting Database complete")
}
// prepDBForDojo
func prepDBForDojo(d *DDConfig, t *targetOS) {
// Preapare the database for DefectDojo by:
// (1) Checking connectivity to the DB,
// (2) checking that the configured Dojo database name doesn't exit already
// (3) Droping the existing database if Drop = true is configured (4) Create the DefectDojo database
// (5) Add the DB user for DefectDojo to use
// TODO: Validate this against @owasp - https://docs.google.com/spreadsheets/d/1HuXh3Zr4mrmb6_YmKkDgzl-ZINYZCvVZn31UCqIGpUA/edit#gid=0
d.sectionMsg("Preparing the database needed for DefectDojo")
err := dbPrep(d, t)
if err != nil {
d.errorMsg(fmt.Sprintf("%+v", err))
os.Exit(1)
}
// Start the installed DB
if d.conf.Install.DB.Local {
d.traceMsg("Starting the local DB")
localDBStart(d, t)
}
}
// dbPrep
func dbPrep(d *DDConfig, t *targetOS) error {
// Call the necessary function for the supported DB engines
switch d.conf.Install.DB.Engine {
case "MySQL":
// Generate commands to install MySQL
return prepMySQL(d, t.id)
case "PostgreSQL":
// Generate commands to install PostgreSQL
return prepPostgreSQL(d, t)
}
// Shouldn't get here but if we do, it's definitely an error
return errors.New("Unknown database engine configured, cannot check connectivity")
}
func prepMySQL(d *DDConfig, osTar string) error {
// TODO: Path check any binaries called
// * mysql
// TODO: Check MySQL version and handle MySQL 8 and password format change
// Set Creds based on dojoConfig.yml
creds := map[string]string{"user": d.conf.Install.DB.Ruser, "pass": d.conf.Install.DB.Rpass}
d.traceMsg(fmt.Sprintf("DB Creds from config are %s / %s", creds["user"], creds["pass"]))
// Creds are unknown if DB is local and newly installed by godojo
if d.conf.Install.DB.Local && !d.conf.Install.DB.Exists {
// Determine default access for fresh install of that OS
// AKA databse is local and didn't exist before the install
creds = defaultDBCreds(d, osTar)
d.addRedact(creds["pass"])
}
d.traceMsg(fmt.Sprintf("DB Creds are now %s / %s", creds["user"], creds["pass"]))
d.statusMsg("Validating DB connection")
// Check connectivity to DB
conCk := sqlStr{
os: osTar,
sql: "SHOW PROCESSLIST;",
errMsg: "Unable to connect to the configured MySQL database",
creds: creds,
kind: "try",
}
_, err := runMySQLCmd(d, conCk)
if err != nil {
d.traceMsg("validation of connection to MySQL failed")
return err
}
// Drop existing DefectDojo database if it exists and configuration says to
if d.conf.Install.DB.Drop {
d.traceMsg("Dropping any existing database per Install.DB.Drop=True in dojoConfig.yml")
// Query MySQL to see if the configured database name exists already
// Another option is "show databases like '" + d.conf.Install.DB.Name + "';"
dbCk := sqlStr{
os: osTar,
sql: "SELECT count(SCHEMA_NAME) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '" +
d.conf.Install.DB.Name + "';",
errMsg: "Unable to check for existing DefectDojo MySQL database",
creds: creds,
kind: "inspect",
}
out, err := runMySQLCmd(d, dbCk)
if err != nil {
d.traceMsg("Check for existing DefectDojo MySQL database failed")
fmt.Println("Drop database set to true but no database found, continuing")
//return err
}
// Clean up stdout from inspectCmd output
strOut := squishSlice(out)
resp := strings.Trim(
strings.ReplaceAll(
strings.ReplaceAll(strOut, "count(SCHEMA_NAME)", ""), "\n", ""), " ")
// Check if there's an existing DB
// if resp = 0 then DB doesn't exist
// if resp = 1 then the DB exists already and needs to be dropped first
ck, err := strconv.Atoi(resp)
if err != nil {
d.traceMsg("Unable to convert existing DB check string to int")
return err
}
if ck == 1 {
d.traceMsg("DB EXISTS so droping that sucker")
dropDB := sqlStr{
os: osTar,
sql: "DROP DATABASE " + d.conf.Install.DB.Name + ";",
errMsg: "Unable to drop the existing MySQL database",
creds: creds,
kind: "try",
}
_, err := runMySQLCmd(d, dropDB)
if err != nil {
d.traceMsg("Failed to drop existing database per configured option to drop existing")
return err
}
fmt.Printf("Existing database %+v dropped since Database Drop was set to %+v\n",
d.conf.Install.DB.Name, d.conf.Install.DB.Drop)
}
}
// Create the DefectDojo database if it doesn't already exist
d.traceMsg("Creating database for DefectDojo on MySQL")
createDB := sqlStr{
os: osTar,
sql: "CREATE DATABASE IF NOT EXISTS " + d.conf.Install.DB.Name + " CHARACTER SET UTF8;",
errMsg: "Unable to create a new MySQL database for DefectDojo",
creds: creds,
kind: "try",
}
_, err = runMySQLCmd(d, createDB)
if err != nil {
d.traceMsg("Failed to create new database for DefectDojo to use")
return err
}
// Drop user DefectDojo uses to connect to the database
d.traceMsg("Dropping existing DefectDojo MySQL DB user, if any")
dropUsr := sqlStr{
os: osTar,
sql: "DROP USER '" + d.conf.Install.DB.User + "'@'localhost';DROP USER '" +
d.conf.Install.DB.User + "'@'%';",
errMsg: "Unable to delete existing database user for DefectDojo or one didn't exist",
creds: creds,
kind: "inspect",
}
out, err := runMySQLCmd(d, dropUsr)
if err != nil {
// No reason to return the error as this is expected for most cases
// and create user will error out for edge cases
d.traceMsg("Unable to delete existing database user for DefectDojo or one didn't exist")
d.traceMsg(fmt.Sprintf("SQL DROP command output was %+v (in any)", squishSlice(out)))
d.traceMsg("Continuing after error deleting user, non-fatal error")
}
// First set the appropriate host for the DefectDojo user to connect from
usrHost := "localhost"
if !d.conf.Install.DB.Local && d.conf.Install.DB.Exists {
// DB is remote and exists so localhost won't work
usrHost = "%"
}
// Create user for DefectDojo to use to connect to the database
d.traceMsg("Creating MySQL DB user for DefectDojo")
createUsr := sqlStr{
os: osTar,
sql: "CREATE USER '" + d.conf.Install.DB.User + "'@'" + usrHost +
"' IDENTIFIED BY '" + d.conf.Install.DB.Pass + "';",
errMsg: "Unable to create a MySQL database user for DefectDojo",
creds: creds,
kind: "try",
}
_, err = runMySQLCmd(d, createUsr)
if err != nil {
d.traceMsg("Failed to create database user for DefectDojo")
return err
}
// Grant the DefectDojo db user the necessary privileges
d.traceMsg("Granting privileges to DefectDojo MySQL DB user")
grantPrivs := sqlStr{
os: osTar,
sql: "GRANT ALL PRIVILEGES ON " + d.conf.Install.DB.Name + ".* TO '" +
d.conf.Install.DB.User + "'@'" + d.conf.Install.DB.Host + "';",
errMsg: "Unable to grant needed privileges to database user for DefectDojo",
creds: creds,
kind: "try",
}
_, err = runMySQLCmd(d, grantPrivs)
if err != nil {
d.traceMsg("Failed to grant privileges to database user for DefectDojo")
return err
}
// Flush privileges to finalize changes to db
d.traceMsg("Flushing privileges for DefectDojo MySQL DB user")
flushPrivs := sqlStr{
os: osTar,
sql: "FLUSH PRIVILEGES;",
errMsg: "Unable to flush database privileges",
creds: creds,
kind: "try",
}
_, err = runMySQLCmd(d, flushPrivs)
if err != nil {
d.traceMsg("Failed to create database user for DefectDojo")
return err
}
// Adjust requirements.txt to only use MySQL Python modules
// TODO: Deprecate this as it's just been a problem generally
err = trimRequirementsTxt(d, "MySQL")
if err != nil {
d.traceMsg("Unable to adjust requirements.txt for MySQL usage, exiting")
return err
}
return nil
}
func runMySQLCmd(d *DDConfig, c sqlStr) ([]string, error) {
out := make([]string, 1)
d.traceMsg(fmt.Sprintf("MySQL query: %s", c.sql))
DBCmds := osCmds{
id: c.os,
cmds: []string{"mysql --host=" + d.conf.Install.DB.Host +
" --user=" + c.creds["user"] +
" --port=" + strconv.Itoa(d.conf.Install.DB.Port) +
" --password=" + c.creds["pass"] +
" --execute=\"" + c.sql + "\""},
errmsg: []string{c.errMsg},
hard: []bool{false},
}
// Switch on how to run the command aka runCmds, tryCmds, inspectCmds
switch c.kind {
case "try":
err := tryCmds(d, DBCmds)
if err != nil {
d.traceMsg(fmt.Sprintf("Error running MySQL command - %s", c.sql))
return out, err
}
case "inspect":
out, err := inspectCmds(d, DBCmds)
if err != nil {
d.traceMsg(fmt.Sprintf("Error running MySQL command - %s", c.sql))
return out, err
}
default:
d.traceMsg("Invalid 'kind' sent to runMySQLCmd, bug in godojo")
fmt.Println("Bug discovered in godojo, see trace message or re-run with trace logging. Quitting.")
os.Exit(1)
}
return out, nil
}
// squishSlice
func squishSlice(sl []string) string {
str := ""
for i := 0; i < len(sl); i++ {
str += sl[i]
}
return str
}
func prepPostgreSQL(d *DDConfig, t *targetOS) error {
// TODO: Path check any binaries called
// * postgres
// Set Creds based on dojoConfig.yml
creds := map[string]string{"user": d.conf.Install.DB.Ruser, "pass": d.conf.Install.DB.Rpass}
d.traceMsg(fmt.Sprintf("DB Creds from config are %s / %s", creds["user"], creds["pass"]))
// Creds are unknown if DB is local and newly installed by godojo
if d.conf.Install.DB.Local && !d.conf.Install.DB.Exists {
// Determine default access for fresh install of that OS
// AKA databse is local and didn't exist before the install
creds = defaultDBCreds(d, t.id)
d.addRedact(creds["pass"])
}
d.traceMsg(fmt.Sprintf("DB Creds are now %s / %s", creds["user"], creds["pass"]))
// Update pg_hba.conf for RHEL only (shakes fist at RHEL)
if !updatePgHba(d, t) {
d.traceMsg("Failed to update pg_hba.conf, cannot connect to the DB. Quiting install")
return errors.New("Unable to update pg_hba.conf so SQL to the DB will fail. Exiting")
}
// Use pg_isready to check connectivity to PostgreSQL DB
d.statusMsg("Checking connectivity to PostgreSQL")
readyOut, err := isPgReady(d, creds)
if err != nil {
d.traceMsg(fmt.Sprintf("PostgreSQL is not available, error was %+v", err))
return err
}
d.traceMsg(fmt.Sprintf("Output from pgReady: %+v", readyOut))
d.statusMsg("Validating DB connection settings")
// Check connectivity to DB
conCk := sqlStr{
os: t.id,
sql: "\\l",
errMsg: "Unable to connect to the configured Postgres database",
creds: creds,
kind: "try",
}
_, err = runPgSQLCmd(d, conCk)
if err != nil {
d.traceMsg("validation of connection to Postgres failed")
// TODO Fix this validation bypass
//return err
}
// Drop existing DefectDojo database if it exists and configuration says to
if d.conf.Install.DB.Drop {
d.traceMsg("Dropping any existing database per Install.DB.Drop=True in dojoConfig.yml")
// Query PostgreSQL to see if the configured database name exists already
dbCk := sqlStr{
os: t.id,
sql: "\\l",
errMsg: "Unable to check for existing DefectDojo PostgreSQL database",
creds: creds,
kind: "inspect",
}
out, err := runPgSQLCmd(d, dbCk)
if err != nil {
d.traceMsg("Check for existing DefectDojo PostgreSQL database failed")
fmt.Println("Drop database set to true but no database found, continuing")
}
// Clean up stdout from inspectCmd output
strOut := squishSlice(out)
ck := pgParseDBList(d, strOut)
// Check if there's an existing DB
// if ck = 0 then DB doesn't exist
// if ck = 1 then the DB exists already and needs to be dropped first
if ck == 1 {
d.traceMsg("DB EXISTS so droping that sucker")
dropDB := sqlStr{
os: t.id,
sql: "DROP DATABASE IF EXISTS " + d.conf.Install.DB.Name + ";",
errMsg: "Unable to drop the existing PostgreSQL database",
creds: creds,
kind: "try",
}
_, err := runPgSQLCmd(d, dropDB)
if err != nil {
d.traceMsg("Failed to drop existing database per configured option to drop existing")
return err
}
fmt.Printf("Existing database %+v dropped since Database Drop was set to %+v\n",
d.conf.Install.DB.Name, d.conf.Install.DB.Drop)
}
}
// Create the DefectDojo database if it doesn't already exist
d.traceMsg("Creating database for DefectDojo on PostgreSQL")
createDB := sqlStr{
os: t.id,
sql: "CREATE DATABASE " + d.conf.Install.DB.Name + ";",
errMsg: "Unable to create a new PostgreSQL database for DefectDojo",
creds: creds,
kind: "try",
}
_, err = runPgSQLCmd(d, createDB)
if err != nil {
d.traceMsg("Failed to create new database for DefectDojo to use")
// TODO: DEGUGGING
//return err
}
// Drop user DefectDojo uses to connect to the database
d.traceMsg("Dropping existing DefectDojo PostgreSQL DB user, if any")
dropUsr := sqlStr{
os: t.id,
sql: "DROP owned by " + d.conf.Install.DB.User + "; DROP USER IF EXISTS " + d.conf.Install.DB.User + ";",
errMsg: "Unable to delete existing database user for DefectDojo or one didn't exist",
creds: creds,
kind: "inspect",
}
out, err := runPgSQLCmd(d, dropUsr)
if err != nil {
// No reason to return the error as this is expected for most cases
// and create user will error out for edge cases
d.traceMsg("Unable to delete existing database user for DefectDojo or one didn't exist")
d.traceMsg(fmt.Sprintf("SQL DROP command output was %+v (in any)", squishSlice(out)))
d.traceMsg("Continuing after error deleting user, non-fatal error")
}
// Create user for DefectDojo to use to connect to the database
d.traceMsg("Creating PostgreSQL DB user for DefectDojo")
createUsr := sqlStr{
os: t.id,
sql: "CREATE USER " + d.conf.Install.DB.User + " WITH ENCRYPTED PASSWORD '" +
d.conf.Install.DB.Pass + "';",
errMsg: "Unable to create a PostgreSQL database user for DefectDojo",
creds: creds,
kind: "try",
}
_, err = runPgSQLCmd(d, createUsr)
if err != nil {
d.traceMsg("Failed to create database user for DefectDojo")
return err
}
// Remote DBs cannot have their pg_hba.conf modified (duh)
if !d.conf.Install.DB.Local {
d.statusMsg("Note: pg_hba.conf has not been altered by godojo.")
d.statusMsg(" It may need to be updated to allow DefectDojo to connect to the DB.")
d.statusMsg(" Please consult the PostgreSQL documentation for further information.")
}
// Grant the DefectDojo db user the necessary privileges
d.traceMsg("Granting privileges to DefectDojo PostgreSQL DB user")
grantPrivs := sqlStr{
os: t.id,
sql: "GRANT ALL PRIVILEGES ON DATABASE " + d.conf.Install.DB.Name + " TO " + d.conf.Install.DB.User + ";",
errMsg: "Unable to grant needed privileges to database user for DefectDojo",
creds: creds,
kind: "try",
}
_, err = runPgSQLCmd(d, grantPrivs)
if err != nil {
d.traceMsg("Failed to grant privileges to database user for DefectDojo")
return err
}
// Set the DefectDojo db user as the owner of dojodb
d.traceMsg("Seting DefectDojo db user as the owner of the DB")
setPrivs := sqlStr{
os: t.id,
sql: "ALTER DATABASE " + d.conf.Install.DB.Name + " OWNER TO " + d.conf.Install.DB.User + ";",
errMsg: "Unable to set database user as owner of DefectDojo DB",
creds: creds,
kind: "try",
}
_, err = runPgSQLCmd(d, setPrivs)
if err != nil {
d.traceMsg("Failed to set ownership to database user for DefectDojo DB")
return err
}
// Adjust requirements.txt to only use MySQL Python modules
err = trimRequirementsTxt(d, "PostgreSQL")
if err != nil {
d.traceMsg("Unable to adjust requirements.txt for PostgreSQL usage, exiting")
return err
}
return nil
}
func runPgSQLCmd(d *DDConfig, c sqlStr) ([]string, error) {
out := make([]string, 1)
d.traceMsg(fmt.Sprintf("Postgres query: %s", c.sql))
DBCmds := osCmds{
id: c.os,
cmds: []string{"sudo -i -u postgres PGPASSWORD=\"" + c.creds["pass"] + "\"" +
" psql --host=" + d.conf.Install.DB.Host +
" --username=" + c.creds["user"] +
" --port=" + strconv.Itoa(d.conf.Install.DB.Port) +
" --command=\"" + c.sql + "\""},
errmsg: []string{c.errMsg},
hard: []bool{false},
}
// Swicht on how to run the command aka runCmds, tryCmds, inspectCmds
switch c.kind {
case "try":
err := tryCmds(d, DBCmds)
// Handle errors from running the PostgreSQL command
if err != nil {
d.traceMsg(fmt.Sprintf("Error running Posgres command - %s", c.sql))
return out, err
}
case "inspect":
out, err := inspectCmds(d, DBCmds)
// Handle errors from running the PostgreSQL command
if err != nil {
d.traceMsg(fmt.Sprintf("Error running Posgres command - %s", c.sql))
return out, err
}
default:
d.traceMsg("Invalid 'kind' sent to runPgSQLCmd, bug in godojo")
fmt.Println("Bug discovered in godojo, see trace message. Quitting.")
os.Exit(1)
}
return out, nil
}
func updatePgHba(d *DDConfig, t *targetOS) bool {
// Only RHEL and binary compatible distros (e.g. Rocky Linux) need to have pg_hba.conf modified)
if !strings.Contains(t.distro, "rhel") {
// return early
return true
}
// For remote DBs, it's not possible to edit pg_hba.conf
if !d.conf.Install.DB.Local {
// return early
return true
}
d.traceMsg("RHEL or variant - pg_hba.conf needs to be updated.")
f, err := os.OpenFile("/var/lib/pgsql/data/pg_hba.conf", os.O_RDWR, 0600)
if err != nil {
// Exit with error code if we can't read the default creds file
d.errorMsg("Unable to read pg_hba.conf file, cannot continue")
os.Exit(1)
}
defer f.Close()
//// Create a new bufferend reader
fr := bufio.NewReader(f)
// Use a scanner to run through the config file to update access
scanner := bufio.NewScanner(fr)
content := ""
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "127.0.0.1/32") {
line = strings.Replace(line, "ident", "md5", 1)
d.traceMsg("Replaced IPv4 localhost")
}
if strings.Contains(line, "::1/128") {
line = strings.Replace(line, "ident", "md5", 1)
d.traceMsg("Replaced IPv6 localhost")
}
content += line + "\n"
}
if err = scanner.Err(); err != nil {
// Exit with error code if we can't scan the default creds file
d.errorMsg("Unable to scan the pg_hba.conf file, exiting")
os.Exit(1)
}
// Truncate the file to make sure its empty before writing
_ = f.Truncate(0)
// Write new config file by starting at the begining of the file
_, err = f.WriteAt([]byte(content), 0)
if err != nil {
// Exit with error code if we can't scan the default creds file
d.errorMsg("Unable to write the pg_hba.conf file, exiting")
os.Exit(1)
}
d.traceMsg("Wrote the updated config file")
// Reload pg_hba.conf using a SQL statement
d.traceMsg("Re-reading the pg_hba.conf file")
creds := map[string]string{"user": d.conf.Install.DB.Ruser, "pass": d.conf.Install.DB.Rpass}
DBCmds := osCmds{
id: t.id,
cmds: []string{"sudo -i -u postgres PGPASSWORD=\"" + creds["pass"] + "\"" +
" psql " + " --username=" + creds["user"] +
" --port=" + strconv.Itoa(d.conf.Install.DB.Port) +
" --command=\"SELECT pg_reload_conf();\""},
errmsg: []string{"Unable to reload the pg_hba.conf file"},
hard: []bool{false},
}
err = tryCmds(d, DBCmds)
if err != nil {
d.traceMsg("Unable to reload the pg_hba.conf file")
d.errorMsg("Unable to reload the pg_hba.conf file, exiting")
os.Exit(1)
}
d.traceMsg("Restarted PostgreSQL")
return true
}
// TODO: REPLACE THIS WITH CLIENT CALLS
func isPgReady(d *DDConfig, creds map[string]string) (string, error) {
d.traceMsg("isPgReady called")
// Call ps_isready and check exit code
pgReady := osCmds{
id: "Linux", cmds: []string{"PGPASSWORD=\"" + creds["pass"] + "\" pg_isready" +
" --host=" + d.conf.Install.DB.Host +
" --username=" + creds["user"] +
" --port=" + strconv.Itoa(d.conf.Install.DB.Port) + " "},
errmsg: []string{"Unable to run pg_isready to validate PostgreSQL DB status."},
hard: []bool{false},
}
d.traceMsg(fmt.Sprintf("Running command: %+v", pgReady.cmds))
out, err := inspectCmds(d, pgReady)
if err != nil {
d.traceMsg(fmt.Sprintf("Error running pg_isready was: %+v", err))
// TODO Fix this error bypass
return squishSlice(out), nil
//return "", err
}
return squishSlice(out), nil
}
// Parse a list of existng PostgreSQL DBs for a specific DB name
// if the DB name is found, return 1 else return 0
func pgParseDBList(d *DDConfig, tbl string) int {
d.traceMsg(fmt.Sprintf("Parsing DB list for existing DefectDojo DB named %+v", d.conf.Install.DB.Name))
// Create a slice for matches
matches := make([]string, 1)
// Split up the string by newlines
lines := strings.Split(tbl, "\n")
for _, l := range lines {
trim := strings.TrimLeft(l, " ")
if len(trim) > 1 {
// Look at the first character
// Possibly a lossy match but doubtful
switch trim[0:1] {
case "L":
continue
case "N":
continue
case "-":
continue
case "|":
continue
case "(":
continue
default:
cells := strings.Split(trim, "|")
matches = append(matches, strings.TrimSpace(cells[0]))
}
}
}
// Look for matches and return 1 if a match is found
for _, v := range matches {
if strings.Contains(v, d.conf.Install.DB.Name) {
d.traceMsg("Match found, there is an existing DefectDojo DB")
return 1
}
}
d.traceMsg("No match found, no existing DefectDojo DB")
return 0
}
func trimRequirementsTxt(d *DDConfig, dbUsed string) error {
d.traceMsg("Called trimRequirementsTxt")
d.traceMsg("WARNING trimRequirementsTxt is deprecated")
return nil
}
func defaultDBCreds(d *DDConfig, os string) map[string]string {
// Setup a map to return
creds := map[string]string{"user": "foo", "pass": "bar"}
getDefaultDBCreds(d, creds)
return creds
}
// Determine the default creds for a database freshly installed in Ubuntu
func getDefaultDBCreds(d *DDConfig, creds map[string]string) {
// Installer currently assumes the default DB passwrod handling won't change by release
// Switch on the DB type
switch d.conf.Install.DB.Engine {
case "MySQL":
ubuntuDefaultMySQL(d, creds)
d.warnMsg("MySQL default credentials are not implemented for RHEL Linux")
case "PostgreSQL":
// Set creds as the Ruser & Rpass for Postgres
creds["user"] = d.conf.Install.DB.Ruser
creds["pass"] = d.conf.Install.DB.Rpass
setDefaultPgSQL(d, creds)
}
}
func ubuntuDefaultMySQL(d *DDConfig, c map[string]string) {
// Sent some initial values that ensure the connection will fail if the file read fails
c["user"] = "debian-sys-maint"
c["pass"] = "FAIL"
// Pull the debian-sys-maint creds from /etc/mysql/debian.cnf
f, err := os.Open("/etc/mysql/debian.cnf")
if err != nil {
// Exit with error code if we can't read the default creds file
d.errorMsg("Unable to read file with defautl credentials, cannot continue")
os.Exit(1)
}
// Create a new buffered reader
fr := bufio.NewReader(f)
// Create a scanner to run through the lines of the file
scanner := bufio.NewScanner(fr)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "password") {
l := strings.Split(line, "=")
// Make sure there was a "=" in l
if len(l) > 1 {
c["pass"] = strings.Trim(l[1], " ")
break
}
}
}
if err = scanner.Err(); err != nil {
// Exit with error code if we can't scan the default creds file
d.errorMsg("Unable to scan file with defautl credentials, cannot continue")
os.Exit(1)
}
}
func setDefaultPgSQL(d *DDConfig, creds map[string]string) {
d.traceMsg("Called setDefaultPgSQL")
// Set user to postgres as that's the default DB user for any new install
creds["user"] = "postgres"
// Use the default local OS user to set the postgres DB user
pgAlter := osCmds{
id: "linux",
cmds: []string{"sudo -i -u postgres psql -c \"ALTER USER postgres with encrypted password '" + creds["pass"] + "';\""},
errmsg: []string{"Unable to set initial password for PostgreSQL database user postgres"},
hard: []bool{false},
}
// Try command
err := tryCmds(d, pgAlter)
if err != nil {
d.traceMsg(fmt.Sprintf("Error updating PostgreSQL DB user with %+v", squishSlice(pgAlter.cmds)))
d.errorMsg("Unable to update default PostgreSQL DB user, quitting")
os.Exit(1)
}
d.traceMsg("No error return from setDefaultPgSQL")
}