Compare commits

...
Author SHA1 Message Date
Raf b080e06039 Fixing issue with removal of hosts and an issue where the host ID for loot did not populate in the database 2023-12-15 13:49:17 -05:00
Joe 7d496d9036 Merge pull request #1506 from BishopFox/docs/next
Fix dark mode docs
2023-12-14 18:30:28 -06:00
Joe bc5c1e0d8a Merge pull request #1505 from BishopFox/docs/next
Docs/next
2023-12-14 18:24:09 -06:00
Joe b84796f723 Merge pull request #1478 from BishopFox/2ee9dependabot/go_modules/gorm.io/driver/sqlite-1.5.4
Bump gorm.io/driver/sqlite from 1.5.2 to 1.5.4
2023-12-14 14:01:33 -06:00
Joe 5c206cdd01 Merge pull request #1504 from BishopFox/fix-iocs
IOC Fixes
2023-12-14 12:43:07 -06:00
Raf f2be8a41e7 Fixing associations between database models and IOC data objects so that IOCs are properly recorded; changed recorded IOC hash to the hash of the non-gzipped data 2023-12-14 11:46:36 -05:00
Joe e197ba9438 Merge pull request #1503 from BishopFox/docs/next
Docs/next
2023-12-13 18:25:40 -06:00
dependabot[bot] e9cc6804bf Bump gorm.io/driver/sqlite from 1.5.2 to 1.5.4
Bumps [gorm.io/driver/sqlite](https://github.com/go-gorm/sqlite) from 1.5.2 to 1.5.4.
- [Commits](https://github.com/go-gorm/sqlite/compare/v1.5.2...v1.5.4)

---
updated-dependencies:
- dependency-name: gorm.io/driver/sqlite
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <[email protected]>
2023-11-27 16:52:42 +00:00
17 changed files with 182 additions and 98 deletions

No files matched your search

+1 -1
View File
@@ -225,7 +225,7 @@ func LootExecute(commandOutput []byte, lootName string, sliverCmdName string, cm
lootName = fmt.Sprintf("[%s] %s on %s (%s)", sliverCmdName, shortCommandName, hostName, timeNow)
}
lootMessage := loot.CreateLootMessage(fileName, lootName, clientpb.FileType_TEXT, commandOutput)
lootMessage := loot.CreateLootMessage(con.ActiveTarget.GetHostUUID(), fileName, lootName, clientpb.FileType_TEXT, commandOutput)
loot.SendLootMessage(lootMessage, con)
}
+7 -6
View File
@@ -92,13 +92,14 @@ func PerformDownload(remotePath string, fileName string, cmd *cobra.Command, con
return download, nil
}
func CreateLootMessage(fileName string, lootName string, lootFileType clientpb.FileType, data []byte) *clientpb.Loot {
func CreateLootMessage(hostUUID string, fileName string, lootName string, lootFileType clientpb.FileType, data []byte) *clientpb.Loot {
if lootName == "" {
lootName = fileName
}
lootMessage := &clientpb.Loot{
Name: lootName,
FileType: lootFileType,
Name: lootName,
OriginHostUUID: hostUUID,
FileType: lootFileType,
File: &commonpb.File{
Name: fileName,
Data: data,
@@ -141,7 +142,7 @@ func LootDownload(download *sliverpb.Download, lootName string, fileType clientp
if !download.IsDir {
// filepath.Base does not deal with backslashes correctly in Windows paths, so we have to standardize the path to forward slashes
downloadPath := strings.ReplaceAll(download.Path, "\\", "/")
lootMessage := CreateLootMessage(filepath.Base(downloadPath), lootName, fileType, download.Data)
lootMessage := CreateLootMessage(con.ActiveTarget.GetHostUUID(), filepath.Base(downloadPath), lootName, fileType, download.Data)
SendLootMessage(lootMessage, con)
} else {
// We have to decompress the gzip file first
@@ -190,7 +191,7 @@ func LootDownload(download *sliverpb.Download, lootName string, fileType clientp
*/
fileData, err := io.ReadAll(tarReader)
if err == nil {
lootMessage := CreateLootMessage(filepath.Base(entryHeader.Name), lootName, fileType, fileData)
lootMessage := CreateLootMessage(con.ActiveTarget.GetHostUUID(), filepath.Base(entryHeader.Name), lootName, fileType, fileData)
SendLootMessage(lootMessage, con)
}
}
@@ -198,7 +199,7 @@ func LootDownload(download *sliverpb.Download, lootName string, fileType clientp
}
func LootText(text string, lootName string, lootFileName string, fileType clientpb.FileType, con *console.SliverConsoleClient) {
lootMessage := CreateLootMessage(lootFileName, lootName, fileType, []byte(text))
lootMessage := CreateLootMessage(con.ActiveTarget.GetHostUUID(), lootFileName, lootName, fileType, []byte(text))
SendLootMessage(lootMessage, con)
}
+1 -1
View File
@@ -146,6 +146,6 @@ func LootProcessDump(dump *sliverpb.ProcessDump, lootName string, hostName strin
lootName = dumpFileName
}
lootMessage := loot.CreateLootMessage(dumpFileName, lootName, clientpb.FileType_BINARY, dump.GetData())
lootMessage := loot.CreateLootMessage(con.ActiveTarget.GetHostUUID(), dumpFileName, lootName, clientpb.FileType_BINARY, dump.GetData())
loot.SendLootMessage(lootMessage, con)
}
+1 -1
View File
@@ -137,7 +137,7 @@ func LootScreenshot(screenshot *sliverpb.Screenshot, lootName string, hostName s
lootName = screenshotFileName
}
lootMessage := loot.CreateLootMessage(screenshotFileName, lootName, clientpb.FileType_BINARY, screenshot.GetData())
lootMessage := loot.CreateLootMessage(con.ActiveTarget.GetHostUUID(), screenshotFileName, lootName, clientpb.FileType_BINARY, screenshot.GetData())
loot.SendLootMessage(lootMessage, con)
}
+16
View File
@@ -689,6 +689,11 @@ func (s *ActiveTarget) IsSession() bool {
return s.session != nil
}
// IsBeacon - Is the current target a beacon?
func (s *ActiveTarget) IsBeacon() bool {
return s.beacon != nil
}
// AddObserver - Observers to notify when the active session changes
func (s *ActiveTarget) AddObserver(observer Observer) int {
s.observerID++
@@ -796,6 +801,17 @@ func (s *ActiveTarget) Background() {
}
}
// GetHostUUID - Get the Host's UUID (ID in the database)
func (s *ActiveTarget) GetHostUUID() string {
if s.IsSession() {
return s.session.UUID
} else if s.IsBeacon() {
return s.beacon.UUID
}
return ""
}
// Expose or hide commands if the active target does support them (or not).
// Ex; hide Windows commands on Linux implants, Wireguard tools on HTTP C2, etc.
func (con *SliverConsoleClient) ExposeCommands() {
+1 -1
View File
@@ -58,7 +58,7 @@ require (
gopkg.in/AlecAivazis/survey.v1 v1.8.8
gorm.io/driver/mysql v1.5.1
gorm.io/driver/postgres v1.5.2
gorm.io/driver/sqlite v1.5.2
gorm.io/driver/sqlite v1.5.4
gorm.io/gorm v1.25.5
gvisor.dev/gvisor v0.0.0-20231116203655-c480f66679e6
modernc.org/sqlite v1.27.0
+2 -2
View File
@@ -620,8 +620,8 @@ gorm.io/driver/mysql v1.5.1 h1:WUEH5VF9obL/lTtzjmML/5e6VfFR/788coz2uaVCAZw=
gorm.io/driver/mysql v1.5.1/go.mod h1:Jo3Xu7mMhCyj8dlrb3WoCaRd1FhsVh+yMXb1jUInf5o=
gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=
gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+2 -2
View File
@@ -759,7 +759,7 @@ func HostByHostID(id uuid.UUID) (*clientpb.Host, error) {
}
// HostByHostUUID - Get host by the session's reported HostUUID
func HostByHostUUID(id string) (*clientpb.Host, error) {
func HostByHostUUID(id string) (*models.Host, error) {
if len(id) < 1 {
return nil, ErrRecordNotFound
}
@@ -774,7 +774,7 @@ func HostByHostUUID(id string) (*clientpb.Host, error) {
if err != nil {
return nil, err
}
return host.ToProtobuf(), nil
return &host, nil
}
// IOCByID - Select an IOC by ID
+2 -2
View File
@@ -36,8 +36,8 @@ type Host struct {
OSVersion string // Verbose OS version
Locale string // Detected language code
IOCs []IOC
ExtensionData []ExtensionData
IOCs []IOC `gorm:"foreignKey:HostID;references:HostUUID"`
ExtensionData []ExtensionData `gorm:"foreignKey:HostID;references:HostUUID"`
}
// BeforeCreate - GORM hook
+1 -2
View File
@@ -54,8 +54,7 @@ func (l *LocalBackend) Add(loot *clientpb.Loot) (*clientpb.Loot, error) {
lootLog.Warnf("Failed to find host %s for loot %s", loot.OriginHostUUID, loot.ID)
hostID = uuid.Nil
} else {
id, _ := uuid.FromString(host.ID)
hostID = id
hostID = host.HostUUID
}
dbLoot := &models.Loot{
Name: loot.GetName(),
+15 -4
View File
@@ -29,7 +29,7 @@ import (
"github.com/bishopfox/sliver/server/db"
"github.com/bishopfox/sliver/server/db/models"
"github.com/bishopfox/sliver/server/log"
"github.com/gofrs/uuid"
"github.com/bishopfox/sliver/util/encoders"
)
var (
@@ -189,6 +189,18 @@ func (rpc *Server) MemfilesRm(ctx context.Context, req *sliverpb.MemfilesRmReq)
return resp, nil
}
func hashUploadData(encoder string, data []byte) [32]byte {
if encoder == "gzip" {
decodedData, err := new(encoders.Gzip).Decode(data)
if err != nil {
return sha256.Sum256(nil)
}
return sha256.Sum256(decodedData)
} else {
return sha256.Sum256(data)
}
}
func trackIOC(req *sliverpb.UploadReq, resp *sliverpb.Upload) {
fsLog.Debugf("Adding IOC to database ...")
request := req.GetRequest()
@@ -207,10 +219,9 @@ func trackIOC(req *sliverpb.UploadReq, resp *sliverpb.Upload) {
return
}
sum := sha256.Sum256(req.Data)
id, _ := uuid.FromString(host.ID)
sum := hashUploadData(req.Encoder, req.Data)
ioc := &models.IOC{
HostID: id,
HostID: host.HostUUID,
Path: resp.Path,
FileHash: fmt.Sprintf("%x", sum),
}
+2 -2
View File
@@ -49,7 +49,7 @@ func (rpc *Server) Host(ctx context.Context, req *clientpb.Host) (*clientpb.Host
if err != nil {
return nil, err
}
return dbHost, nil
return dbHost.ToProtobuf(), nil
}
// HostRm - Remove a host from the database
@@ -58,7 +58,7 @@ func (rpc *Server) HostRm(ctx context.Context, req *clientpb.Host) (*commonpb.Em
if err != nil {
return nil, err
}
err = db.Session().Delete(dbHost).Error
err = db.Session().Delete(*dbHost).Error
if err != nil {
return nil, err
}
-4
View File
@@ -115,10 +115,6 @@ func (rpc *Server) GenericHandler(req GenericRequest, resp GenericResponse) erro
return ErrInvalidSessionID
}
// Overwrite unused implant fields before re-serializing
request.SessionID = ""
request.BeaconID = ""
reqData, err := proto.Marshal(req)
if err != nil {
return err
+55 -1
View File
@@ -125,7 +125,7 @@ func parseDDL(strs ...string) (*ddl, error) {
ColumnTypeValue: sql.NullString{String: matches[2], Valid: true},
PrimaryKeyValue: sql.NullBool{Valid: true},
UniqueValue: sql.NullBool{Valid: true},
NullableValue: sql.NullBool{Valid: true},
NullableValue: sql.NullBool{Bool: true, Valid: true},
DefaultValueValue: sql.NullString{Valid: false},
}
@@ -175,6 +175,18 @@ func parseDDL(strs ...string) (*ddl, error) {
return &result, nil
}
func (d *ddl) clone() *ddl {
copied := new(ddl)
*copied = *d
copied.fields = make([]string, len(d.fields))
copy(copied.fields, d.fields)
copied.columns = make([]migrator.ColumnType, len(d.columns))
copy(copied.columns, d.columns)
return copied
}
func (d *ddl) compile() string {
if len(d.fields) == 0 {
return d.head
@@ -183,6 +195,21 @@ func (d *ddl) compile() string {
return fmt.Sprintf("%s (%s)", d.head, strings.Join(d.fields, ","))
}
func (d *ddl) renameTable(dst, src string) error {
tableReg, err := regexp.Compile("\\s*('|`|\")?\\b" + regexp.QuoteMeta(src) + "\\b('|`|\")?\\s*")
if err != nil {
return err
}
replaced := tableReg.ReplaceAllString(d.head, fmt.Sprintf(" `%s` ", dst))
if replaced == d.head {
return fmt.Errorf("failed to look up tablename `%s` from DDL head '%s'", src, d.head)
}
d.head = replaced
return nil
}
func (d *ddl) addConstraint(name string, sql string) {
reg := regexp.MustCompile("^CONSTRAINT [\"`]?" + regexp.QuoteMeta(name) + "[\"` ]")
@@ -240,3 +267,30 @@ func (d *ddl) getColumns() []string {
}
return res
}
func (d *ddl) alterColumn(name, sql string) bool {
reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$")
for i := 0; i < len(d.fields); i++ {
if reg.MatchString(d.fields[i]) {
d.fields[i] = sql
return false
}
}
d.fields = append(d.fields, sql)
return true
}
func (d *ddl) removeColumn(name string) bool {
reg := regexp.MustCompile("^(`|'|\"| )" + regexp.QuoteMeta(name) + "(`|'|\"| ) .*?$")
for i := 0; i < len(d.fields); i++ {
if reg.MatchString(d.fields[i]) {
d.fields = append(d.fields[:i], d.fields[i+1:]...)
return true
}
}
return false
}
+30 -55
View File
@@ -3,7 +3,6 @@ package sqlite
import (
"database/sql"
"fmt"
"regexp"
"strings"
"gorm.io/gorm"
@@ -78,23 +77,16 @@ func (m Migrator) HasColumn(value interface{}, name string) bool {
func (m Migrator) AlterColumn(value interface{}, name string) error {
return m.RunWithoutForeignKey(func() error {
return m.recreateTable(value, nil, func(rawDDL string, stmt *gorm.Statement) (sql string, sqlArgs []interface{}, err error) {
return m.recreateTable(value, nil, func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
if field := stmt.Schema.LookUpField(name); field != nil {
// lookup field from table definition, ddl might looks like `'name' int,` or `'name' int)`
reg, err := regexp.Compile("(`|'|\"| )" + field.DBName + "(`|'|\"| ) .*?(,|\\)\\s*$)")
if err != nil {
return "", nil, err
if ddl.alterColumn(field.DBName, fmt.Sprintf("`%s` ?", field.DBName)) {
return nil, nil, fmt.Errorf("field `%s` not found in origin ddl, ddl= '%s'", name, ddl.compile())
}
createSQL := reg.ReplaceAllString(rawDDL, fmt.Sprintf("`%v` ?$3", field.DBName))
if createSQL == rawDDL {
return "", nil, fmt.Errorf("failed to look up field %v from DDL %v", field.DBName, rawDDL)
}
return createSQL, []interface{}{m.FullDataTypeOf(field)}, nil
return ddl, []interface{}{m.FullDataTypeOf(field)}, nil
}
return "", nil, fmt.Errorf("failed to alter field with name %v", name)
return nil, nil, fmt.Errorf("failed to alter field with name `%s`", name)
})
})
}
@@ -149,19 +141,13 @@ func (m Migrator) ColumnTypes(value interface{}) ([]gorm.ColumnType, error) {
}
func (m Migrator) DropColumn(value interface{}, name string) error {
return m.recreateTable(value, nil, func(rawDDL string, stmt *gorm.Statement) (sql string, sqlArgs []interface{}, err error) {
return m.recreateTable(value, nil, func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
if field := stmt.Schema.LookUpField(name); field != nil {
name = field.DBName
}
reg, err := regexp.Compile("(`|'|\"| |\\[)" + name + "(`|'|\"| |\\]) .*?,")
if err != nil {
return "", nil, err
}
createSQL := reg.ReplaceAllString(rawDDL, "")
return createSQL, nil, nil
ddl.removeColumn(name)
return ddl, nil, nil
})
}
@@ -170,7 +156,7 @@ func (m Migrator) CreateConstraint(value interface{}, name string) error {
constraint, chk, table := m.GuessConstraintAndTable(stmt, name)
return m.recreateTable(value, &table,
func(rawDDL string, stmt *gorm.Statement) (sql string, sqlArgs []interface{}, err error) {
func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
var (
constraintName string
constraintSql string
@@ -185,17 +171,11 @@ func (m Migrator) CreateConstraint(value interface{}, name string) error {
constraintSql = "CONSTRAINT ? CHECK (?)"
constraintValues = []interface{}{clause.Column{Name: chk.Name}, clause.Expr{SQL: chk.Constraint}}
} else {
return "", nil, nil
return nil, nil, nil
}
createDDL, err := parseDDL(rawDDL)
if err != nil {
return "", nil, err
}
createDDL.addConstraint(constraintName, constraintSql)
createSQL := createDDL.compile()
return createSQL, constraintValues, nil
ddl.addConstraint(constraintName, constraintSql)
return ddl, constraintValues, nil
})
})
}
@@ -210,15 +190,9 @@ func (m Migrator) DropConstraint(value interface{}, name string) error {
}
return m.recreateTable(value, &table,
func(rawDDL string, stmt *gorm.Statement) (sql string, sqlArgs []interface{}, err error) {
createDDL, err := parseDDL(rawDDL)
if err != nil {
return "", nil, err
}
createDDL.removeConstraint(name)
createSQL := createDDL.compile()
return createSQL, nil, nil
func(ddl *ddl, stmt *gorm.Statement) (*ddl, []interface{}, error) {
ddl.removeConstraint(name)
return ddl, nil, nil
})
})
}
@@ -375,8 +349,10 @@ func (m Migrator) getRawDDL(table string) (string, error) {
return createSQL, nil
}
func (m Migrator) recreateTable(value interface{}, tablePtr *string,
getCreateSQL func(rawDDL string, stmt *gorm.Statement) (sql string, sqlArgs []interface{}, err error)) error {
func (m Migrator) recreateTable(
value interface{}, tablePtr *string,
getCreateSQL func(ddl *ddl, stmt *gorm.Statement) (sql *ddl, sqlArgs []interface{}, err error),
) error {
return m.RunWithValue(value, func(stmt *gorm.Statement) error {
table := stmt.Table
if tablePtr != nil {
@@ -388,27 +364,26 @@ func (m Migrator) recreateTable(value interface{}, tablePtr *string,
return err
}
newTableName := table + "__temp"
createSQL, sqlArgs, err := getCreateSQL(rawDDL, stmt)
originDDL, err := parseDDL(rawDDL)
if err != nil {
return err
}
if createSQL == "" {
createDDL, sqlArgs, err := getCreateSQL(originDDL.clone(), stmt)
if err != nil {
return err
}
if createDDL == nil {
return nil
}
tableReg, err := regexp.Compile("\\s*('|`|\")?\\b" + table + "\\b('|`|\")?\\s*")
if err != nil {
newTableName := table + "__temp"
if err := createDDL.renameTable(newTableName, table); err != nil {
return err
}
createSQL = tableReg.ReplaceAllString(createSQL, fmt.Sprintf(" `%v` ", newTableName))
createDDL, err := parseDDL(createSQL)
if err != nil {
return err
}
columns := createDDL.getColumns()
createSQL := createDDL.compile()
return m.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Exec(createSQL, sqlArgs...).Error; err != nil {
+45 -13
View File
@@ -4,7 +4,6 @@ import (
"context"
"database/sql"
"strconv"
"strings"
"gorm.io/gorm/callbacks"
@@ -56,7 +55,7 @@ func (dialector Dialector) Initialize(db *gorm.DB) (err error) {
if compareVersion(version, "3.35.0") >= 0 {
callbacks.RegisterDefaultCallbacks(db, &callbacks.Config{
CreateClauses: []string{"INSERT", "VALUES", "ON CONFLICT", "RETURNING"},
UpdateClauses: []string{"UPDATE", "SET", "WHERE", "RETURNING"},
UpdateClauses: []string{"UPDATE", "SET", "FROM", "WHERE", "RETURNING"},
DeleteClauses: []string{"DELETE", "FROM", "WHERE", "RETURNING"},
LastInsertIDReversed: true,
})
@@ -143,19 +142,51 @@ func (dialector Dialector) BindVarTo(writer clause.Writer, stmt *gorm.Statement,
}
func (dialector Dialector) QuoteTo(writer clause.Writer, str string) {
writer.WriteByte('`')
if strings.Contains(str, ".") {
for idx, str := range strings.Split(str, ".") {
if idx > 0 {
writer.WriteString(".`")
var (
underQuoted, selfQuoted bool
continuousBacktick int8
shiftDelimiter int8
)
for _, v := range []byte(str) {
switch v {
case '`':
continuousBacktick++
if continuousBacktick == 2 {
writer.WriteString("``")
continuousBacktick = 0
}
writer.WriteString(str)
writer.WriteByte('`')
case '.':
if continuousBacktick > 0 || !selfQuoted {
shiftDelimiter = 0
underQuoted = false
continuousBacktick = 0
writer.WriteString("`")
}
writer.WriteByte(v)
continue
default:
if shiftDelimiter-continuousBacktick <= 0 && !underQuoted {
writer.WriteString("`")
underQuoted = true
if selfQuoted = continuousBacktick > 0; selfQuoted {
continuousBacktick -= 1
}
}
for ; continuousBacktick > 0; continuousBacktick -= 1 {
writer.WriteString("``")
}
writer.WriteByte(v)
}
} else {
writer.WriteString(str)
writer.WriteByte('`')
shiftDelimiter++
}
if continuousBacktick > 0 && !selfQuoted {
writer.WriteString("``")
}
writer.WriteString("`")
}
func (dialector Dialector) Explain(sql string, vars ...interface{}) string {
@@ -167,7 +198,8 @@ func (dialector Dialector) DataTypeOf(field *schema.Field) string {
case schema.Bool:
return "numeric"
case schema.Int, schema.Uint:
if field.AutoIncrement && !field.PrimaryKey {
if field.AutoIncrement {
// doesn't check `PrimaryKey`, to keep backward compatibility
// https://www.sqlite.org/autoinc.html
return "integer PRIMARY KEY AUTOINCREMENT"
} else {
+1 -1
View File
@@ -1027,7 +1027,7 @@ gorm.io/driver/mysql
# gorm.io/driver/postgres v1.5.2
## explicit; go 1.18
gorm.io/driver/postgres
# gorm.io/driver/sqlite v1.5.2
# gorm.io/driver/sqlite v1.5.4
## explicit; go 1.20
gorm.io/driver/sqlite
# gorm.io/gorm v1.25.5