Files
trufflehog/pkg/analyzer/analyzers/mysql/mysql.go

976 lines
25 KiB
Go

//go:generate generate_permissions permissions.yaml permissions.go mysql
package mysql
import (
"database/sql"
"fmt"
"os"
"strings"
"time"
"github.com/dustin/go-humanize"
_ "github.com/go-sql-driver/mysql"
"github.com/jedib0t/go-pretty/text"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/xo/dburl"
"github.com/fatih/color"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/analyzers"
"github.com/trufflesecurity/trufflehog/v3/pkg/analyzer/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
)
var _ analyzers.Analyzer = (*Analyzer)(nil)
type Analyzer struct {
Cfg *config.Config
}
func (Analyzer) Type() analyzers.AnalyzerType { return analyzers.AnalyzerTypeMySQL }
func (a Analyzer) Analyze(_ context.Context, credInfo map[string]string) (*analyzers.AnalyzerResult, error) {
uri, ok := credInfo["connection_string"]
if !ok {
return nil, fmt.Errorf("missing connection string")
}
info, err := AnalyzePermissions(a.Cfg, uri)
if err != nil {
return nil, err
}
return secretInfoToAnalyzerResult(info), nil
}
func secretInfoToAnalyzerResult(info *SecretInfo) *analyzers.AnalyzerResult {
if info == nil {
return nil
}
result := analyzers.AnalyzerResult{
AnalyzerType: analyzers.AnalyzerTypeMySQL,
Metadata: nil,
Bindings: []analyzers.Binding{},
}
// add user privileges to bindings
userBindings, userResource := bakeUserBindings(info)
result.Bindings = append(result.Bindings, userBindings...)
// add user's database privileges to bindings
databaseBindings := bakeDatabaseBindings(userResource, info)
result.Bindings = append(result.Bindings, databaseBindings...)
return &result
}
func bakeUserBindings(info *SecretInfo) ([]analyzers.Binding, *analyzers.Resource) {
var userBindings []analyzers.Binding
// add user and their privileges to bindings
userResource := analyzers.Resource{
Name: info.User,
FullyQualifiedName: info.Host + "/" + info.User,
Type: "user",
}
for _, priv := range info.GlobalPrivs.Privs {
userBindings = append(userBindings, analyzers.Binding{
Resource: userResource,
Permission: analyzers.Permission{
Value: priv,
},
})
}
return userBindings, &userResource
}
func bakeDatabaseBindings(userResource *analyzers.Resource, info *SecretInfo) []analyzers.Binding {
var databaseBindings []analyzers.Binding
for _, database := range info.Databases {
dbResource := analyzers.Resource{
Name: database.Name,
FullyQualifiedName: info.Host + "/" + database.Name,
Type: "database",
Metadata: map[string]any{
"default": database.Default,
"non_existent": database.Nonexistent,
},
Parent: userResource,
}
for _, priv := range database.Privs {
databaseBindings = append(databaseBindings, analyzers.Binding{
Resource: dbResource,
Permission: analyzers.Permission{
Value: priv,
},
})
}
// add this database's table privileges to bindings
tableBindings := bakeTableBindings(&dbResource, database)
databaseBindings = append(databaseBindings, tableBindings...)
// add this database's routines privileges to bindings
routineBindings := bakeRoutineBindings(&dbResource, database)
databaseBindings = append(databaseBindings, routineBindings...)
}
return databaseBindings
}
func bakeTableBindings(dbResource *analyzers.Resource, database *Database) []analyzers.Binding {
if database.Tables == nil {
return nil
}
var tableBindings []analyzers.Binding
for _, table := range *database.Tables {
tableResource := analyzers.Resource{
Name: table.Name,
FullyQualifiedName: dbResource.FullyQualifiedName + "/" + table.Name,
Type: "table",
Metadata: map[string]any{
"bytes": table.Bytes,
"non_existent": table.Nonexistent,
},
Parent: dbResource,
}
for _, priv := range table.Privs {
tableBindings = append(tableBindings, analyzers.Binding{
Resource: tableResource,
Permission: analyzers.Permission{
Value: priv,
},
})
}
// Add this table's column privileges to bindings
for _, column := range table.Columns {
columnResource := analyzers.Resource{
Name: column.Name,
FullyQualifiedName: tableResource.FullyQualifiedName + "/" + column.Name,
Type: "column",
Parent: &tableResource,
}
for _, priv := range column.Privs {
tableBindings = append(tableBindings, analyzers.Binding{
Resource: columnResource,
Permission: analyzers.Permission{
Value: priv,
},
})
}
}
}
return tableBindings
}
func bakeRoutineBindings(dbResource *analyzers.Resource, database *Database) []analyzers.Binding {
if database.Routines == nil {
return nil
}
var routineBindings []analyzers.Binding
for _, routine := range *database.Routines {
routineResource := analyzers.Resource{
Name: routine.Name,
FullyQualifiedName: dbResource.FullyQualifiedName + "/" + routine.Name,
Type: "routine",
Metadata: map[string]any{
"non_existent": routine.Nonexistent,
},
Parent: dbResource,
}
for _, priv := range routine.Privs {
routineBindings = append(routineBindings, analyzers.Binding{
Resource: routineResource,
Permission: analyzers.Permission{
Value: priv,
},
})
}
}
return routineBindings
}
const (
// MySQL SSL Modes
mysql_sslmode = "ssl-mode"
mysql_sslmode_disabled = "DISABLED"
mysql_sslmode_preferred = "PREFERRED"
mysql_sslmode_required = "REQUIRED"
mysql_sslmode_verify_ca = "VERIFY_CA"
mysql_sslmode_verify_identity = "VERIFY_IDENTITY"
//https://github.com/go-sql-driver/mysql/issues/899#issuecomment-443493840
// MySQL Built-in Databases
mysql_db_sys = "sys"
mysql_db_perf_sch = "performance_schema"
mysql_db_info_sch = "information_schema"
mysql_db_mysql = "mysql"
mysql_all = "*"
)
type GlobalPrivs struct {
Privs []string
}
type Database struct {
Name string
Default bool
Tables *[]Table
Privs []string
Routines *[]Routine
Nonexistent bool
}
type Table struct {
Name string
Columns []Column
Privs []string
Nonexistent bool
Bytes uint64
}
type Column struct {
Name string
Privs []string
}
type Routine struct {
Name string
Privs []string
Nonexistent bool
}
// so CURRENT_USER returns `doadmin@%` and not `doadmin@localhost
// USER() returns `doadmin@localhost`
type SecretInfo struct {
Host string
User string
Databases map[string]*Database
GlobalPrivs GlobalPrivs
}
func AnalyzeAndPrintPermissions(cfg *config.Config, key string) {
// ToDo: Add in logging
if cfg.LoggingEnabled {
color.Red("[x] Logging is not supported for this analyzer.")
return
}
info, err := AnalyzePermissions(cfg, key)
if err != nil {
color.Red("[x] Error: %s", err.Error())
return
}
color.Green("[+] Successfully connected as user: %s", info.User)
// Print the results
printResults(info.Databases, info.GlobalPrivs, cfg.ShowAll)
}
func AnalyzePermissions(cfg *config.Config, connectionStr string) (*SecretInfo, error) {
// Parse the connection string
u, err := parseConnectionStr(connectionStr)
if err != nil {
return nil, fmt.Errorf("parsing the connection string: %w", err)
}
db, err := createConnection(u)
if err != nil {
return nil, fmt.Errorf("connecting to the MySQL database: %w", err)
}
defer db.Close()
// Get the current user
user, err := getUser(db)
if err != nil {
return nil, fmt.Errorf("getting the current user: %w", err)
}
// Get all accessible databases
var databases = make(map[string]*Database, 0)
err = getDatabases(db, databases)
if err != nil {
return nil, fmt.Errorf("getting databases: %w", err)
}
//Get all accessible tables
err = getTables(db, databases)
if err != nil {
return nil, fmt.Errorf("getting tables: %w", err)
}
// Get user grants
grants, err := getGrants(db)
if err != nil {
return nil, fmt.Errorf("getting user grants: %w", err)
}
// Get all accessible routines
err = getRoutines(db, databases)
if err != nil {
return nil, fmt.Errorf("getting routines: %w", err)
}
var globalPrivs GlobalPrivs
// Process user grants
processGrants(grants, databases, &globalPrivs)
return &SecretInfo{
Host: u.Hostname(),
User: user,
Databases: databases,
GlobalPrivs: globalPrivs,
}, nil
}
func parseConnectionStr(connection string) (*dburl.URL, error) {
// Check if the connection string starts with 'mysql://'
if !strings.HasPrefix(connection, "mysql://") {
color.Yellow("[i] The connection string should start with 'mysql://'. Adding it for you.")
connection = "mysql://" + connection
}
// Adapt ssl-mode params to Go MySQL driver
connection, err := fixTLSQueryParam(connection)
if err != nil {
return nil, err
}
// Parse the connection string
u, err := dburl.Parse(connection)
if err != nil {
return nil, err
}
return u, nil
}
func createConnection(u *dburl.URL) (*sql.DB, error) {
// Connect to the MySQL database
db, err := sql.Open("mysql", u.DSN)
if err != nil {
return nil, err
}
db.SetConnMaxLifetime(time.Minute * 5)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
// Check the connection
err = db.Ping()
if err != nil {
if strings.Contains(err.Error(), "certificate signed by unknown authority") {
return nil, fmt.Errorf("%s. try adding 'ssl-mode=PREFERRED' to your connection string", err.Error())
}
return nil, err
}
return db, nil
}
func fixTLSQueryParam(connection string) (string, error) {
// Parse connection string on "?"
parsed := strings.Split(connection, "?")
// Check if has query parms
if len(parsed) < 2 {
// Add 10s timeout
connection += "?timeout=10s"
return connection, nil
}
var error error
// Split parms
querySlice := strings.Split(parsed[1], "&")
// Check if ssl-mode is present
for i, part := range querySlice {
if strings.HasPrefix(part, "ssl-mode") {
mode := strings.Split(part, "=")[1]
switch mode {
case mysql_sslmode_disabled:
querySlice[i] = "tls=false"
case mysql_sslmode_preferred:
querySlice[i] = "tls=preferred"
case mysql_sslmode_required:
querySlice[i] = "tls=true"
case mysql_sslmode_verify_ca:
error = fmt.Errorf("this implementation does not support VERIFY_CA. try removing it or using ssl-mode=REQUIRED")
// Need to implement --ssl-ca or --ssl-capath
case mysql_sslmode_verify_identity:
error = fmt.Errorf("this implementation does not support VERIFY_IDENTITY. try removing it or using ssl-mode=REQUIRED")
// Need to implement --ssl-ca or --ssl-capath
}
}
}
// Join the parts back together
newQuerySlice := strings.Join(querySlice, "&")
return (parsed[0] + "?" + newQuerySlice + "&timeout=10s"), error
}
func getUser(db *sql.DB) (string, error) {
var user string
err := db.QueryRow("SELECT CURRENT_USER()").Scan(&user)
if err != nil {
return "", err
}
return user, nil
}
func getDatabases(db *sql.DB, databases map[string]*Database) error {
rows, err := db.Query("SHOW DATABASES")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var dbName string
err = rows.Scan(&dbName)
if err != nil {
return err
}
// check if the database is a built-in database
built_in_db := false
switch dbName {
case mysql_db_sys, mysql_db_perf_sch, mysql_db_info_sch, mysql_db_mysql:
built_in_db = true
}
// add the database to the databases map
newTables := make([]Table, 0)
newRoutines := make([]Routine, 0)
databases[dbName] = &Database{Name: dbName, Default: built_in_db, Tables: &newTables, Routines: &newRoutines}
}
return nil
}
func getTables(db *sql.DB, databases map[string]*Database) error {
rows, err := db.Query("SELECT table_schema, table_name, IFNULL(DATA_LENGTH,0) FROM information_schema.tables")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var dbName string
var tableName string
var tableSize uint64
err = rows.Scan(&dbName, &tableName, &tableSize)
if err != nil {
return err
}
// find the database in the databases slice
d := databases[dbName]
*d.Tables = append(*d.Tables, Table{Name: tableName, Bytes: tableSize})
}
return nil
}
func getRoutines(db *sql.DB, databases map[string]*Database) error {
rows, err := db.Query("SELECT routine_schema, routine_name FROM information_schema.routines")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var dbName string
var routineName string
err = rows.Scan(&dbName, &routineName)
if err != nil {
return err
}
// find the database in the databases slice
d, ok := databases[dbName]
if !ok {
databases[dbName] = &Database{Name: dbName, Default: false, Tables: &[]Table{}, Routines: &[]Routine{}, Nonexistent: true}
d = databases[dbName]
}
*d.Routines = append(*d.Routines, Routine{Name: routineName})
}
return nil
}
func getGrants(db *sql.DB) ([]string, error) {
rows, err := db.Query("SHOW GRANTS")
if err != nil {
return nil, err
}
defer rows.Close()
var grants []string
for rows.Next() {
var grant string
err = rows.Scan(&grant)
if err != nil {
return nil, err
}
grants = append(grants, grant)
}
return grants, nil
}
// ToDo: Deal with these GRANT/REVOKE statements
// GRANT SELECT (col1), INSERT (col1, col2) ON mydb.mytbl TO 'someuser'@'somehost';
// GRANT PROXY ON 'localuser'@'localhost' TO 'externaluser'@'somehost';
// GRANT 'role1', 'role2' TO 'user1'@'localhost', 'user2'@'localhost';
// What are the default privs on information_schema and performance_Schema?
// Seems table by table...maybe just put "Not Implemented" and leave this to be a show_all option.
// Note: Can't GRANT on a table that doesn't exist, but DB is fine.
// processGrants processes the grants and adds them to the databases structs and globalPrivs
func processGrants(grants []string, databases map[string]*Database, globalPrivs *GlobalPrivs) {
for _, grant := range grants {
// GRANTs on non-existent databases are valid, but we need that object to exist in "databases" for processGrant().
db := parseDBFromGrant(grant)
if db == mysql_all {
continue
}
_, ok := databases[db]
if !ok {
databases[db] = &Database{Name: db, Default: false, Tables: &[]Table{}, Routines: &[]Routine{}, Nonexistent: true}
}
}
for _, grant := range grants {
// TODO: How to deal with error here?
_ = processGrant(grant, databases, globalPrivs)
}
}
func processGrant(grant string, databases map[string]*Database, globalPrivs *GlobalPrivs) error {
isGrant := strings.HasPrefix(grant, "GRANT")
//hasGrantOption := strings.HasSuffix(grant, "WITH GRANT OPTION")
// remove GRANT or REVOKE
grant = strings.TrimPrefix(grant, "GRANT")
grant = strings.TrimPrefix(grant, "REVOKE")
// Split on " ON "
parts := strings.Split(grant, " ON ")
if len(parts) < 2 {
return fmt.Errorf("Error processing grant: %s", grant)
}
// Put privs in a slice
privs := strings.Split(parts[0], ",")
for i, priv := range privs {
privs[i] = strings.Trim(priv, " ")
}
// Get DB and Table
dbName := strings.Trim(strings.Split(parts[1], " TO ")[0], " ")
if dbName == parts[1] {
dbName = strings.Trim(strings.Split(parts[1], " FROM ")[0], " ")
}
// Find the database in the databases slice
// Note: table may not exist yet OR may be a routine
dbTableParts := strings.Split(dbName, ".")
db := strings.Trim(dbTableParts[0], "\"`")
table := strings.Trim(dbTableParts[1], "\"`")
// dont' forget to deal with revoking db-level privs
if db == mysql_all {
// Deal with "ALL" and "ALL PRIVILEGES"
switch privs[0] {
case "ALL", "ALL PRIVILEGES":
addRemoveAllPrivs(databases, globalPrivs, isGrant)
default:
for _, priv := range privs {
addRemoveOnePrivOnAll(databases, globalPrivs, priv, isGrant)
}
}
} else {
// Check if the privs are for a routine
isRoutine := checkIsRoutine(privs)
if isRoutine {
db = strings.TrimPrefix(db, "PROCEDURE `")
db = strings.TrimSuffix(db, "`")
}
d := databases[db]
switch {
case table == mysql_all:
filteredDBPrivs := filterDBPrivs(privs)
filteredTablePrivs := filterTablePrivs(privs)
d.Privs = addRemovePrivs(d.Privs, filteredDBPrivs, isGrant)
for i, t := range *d.Tables {
(*d.Tables)[i].Privs = addRemovePrivs(t.Privs, filteredTablePrivs, isGrant)
}
case isRoutine:
var idx = getRoutineIndex(d, table)
if idx == -1 {
*d.Routines = append(*d.Routines, Routine{Name: table, Nonexistent: true})
idx = len(*d.Routines) - 1
}
(*d.Routines)[idx].Privs = addRemovePrivs((*d.Routines)[idx].Privs, privs, isGrant)
default:
var idx = getTableIndex(d, table)
if idx == -1 {
*d.Tables = append(*d.Tables, Table{Name: table, Nonexistent: true, Bytes: 0})
idx = len(*d.Tables) - 1
}
(*d.Tables)[idx].Privs = addRemovePrivs((*d.Tables)[idx].Privs, privs, isGrant)
}
}
return nil
}
func parseDBFromGrant(grant string) string {
// Split on " ON "
parts := strings.Split(grant, " ON ")
if len(parts) < 2 {
color.Red("[!] Error processing grant: %s", grant)
return ""
}
// Get DB and Table
dbName := strings.Trim(strings.Split(parts[1], " TO ")[0], " ")
if dbName == parts[1] {
dbName = strings.Trim(strings.Split(parts[1], " FROM ")[0], " ")
}
dbTableParts := strings.Split(dbName, ".")
db := strings.Trim(dbTableParts[0], "\"`")
db = strings.TrimPrefix(db, "PROCEDURE `")
db = strings.TrimSuffix(db, "`")
return db
}
func filterDBPrivs(privs []string) []string {
filtered := make([]string, 0)
for _, priv := range privs {
if SCOPES[priv].Database {
filtered = append(filtered, priv)
}
}
return filtered
}
func filterTablePrivs(privs []string) []string {
filtered := make([]string, 0)
for _, priv := range privs {
if SCOPES[priv].Table {
filtered = append(filtered, priv)
}
}
return filtered
}
func addRemoveOnePrivOnAll(databases map[string]*Database, globalPrivs *GlobalPrivs, priv string, isGrant bool) {
scope, ok := SCOPES[priv]
if !ok {
color.Red("[!] Error processing grant: privilege doesn't exist in our MySQL (%s)", priv)
return
}
slicedPriv := []string{priv}
// Add priv to globalPrivs
if scope.Global {
globalPrivs.Privs = addRemovePrivs(globalPrivs.Privs, slicedPriv, isGrant)
}
// Add/Remove priv to all databases
if scope.Database {
for _, d := range databases {
if d.Name == "information_schema" || d.Name == "performance_schema" {
continue
}
d.Privs = addRemovePrivs(d.Privs, slicedPriv, isGrant)
}
}
// Add/Remove priv to all tables
if scope.Table {
for _, d := range databases {
for i, t := range *d.Tables {
(*d.Tables)[i].Privs = addRemovePrivs(t.Privs, slicedPriv, isGrant)
}
}
}
// Add/Remove priv to all routines
if scope.Routine {
for _, d := range databases {
for i, r := range *d.Routines {
(*d.Routines)[i].Privs = addRemovePrivs(r.Privs, slicedPriv, isGrant)
}
}
}
}
func addRemoveAllPrivs(databases map[string]*Database, globalPrivs *GlobalPrivs, isGrant bool) {
// Add all privs to globalPrivs
globalAllPrivs := getGlobalAllPrivileges()
globalPrivs.Privs = addRemovePrivs(globalPrivs.Privs, globalAllPrivs, isGrant)
// Get DB, Table and Routine Privs
dbAllPrivs := getDBAllPrivs()
tableAllPrivs := getTableAllPrivs()
routineAllPrivs := getRoutineAllPrivs()
// Add all privs to all databases and tables and routines
for _, d := range databases {
if d.Name == "information_schema" || d.Name == "performance_schema" {
continue
}
// Add DB-level privs
d.Privs = addRemovePrivs(d.Privs, dbAllPrivs, isGrant)
// Add Table-level privs
for i, t := range *d.Tables {
(*d.Tables)[i].Privs = addRemovePrivs(t.Privs, tableAllPrivs, isGrant)
}
// Add Routine-level privs
for i, r := range *d.Routines {
(*d.Routines)[i].Privs = addRemovePrivs(r.Privs, routineAllPrivs, isGrant)
}
}
}
func getGlobalAllPrivileges() []string {
privs := make([]string, 0)
for priv, scope := range SCOPES {
if scope.Global && !scope.Dynamic && priv != "USAGE" && priv != "GRANT OPTION" {
privs = append(privs, priv)
}
}
return privs
}
func getDBAllPrivs() []string {
privs := make([]string, 0)
for priv, scope := range SCOPES {
if scope.Database && !scope.Dynamic && priv != "USAGE" && priv != "GRANT OPTION" {
privs = append(privs, priv)
}
}
return privs
}
func getTableAllPrivs() []string {
privs := make([]string, 0)
for priv, scope := range SCOPES {
if scope.Table && !scope.Dynamic && priv != "USAGE" && priv != "GRANT OPTION" {
privs = append(privs, priv)
}
}
return privs
}
func getRoutineAllPrivs() []string {
privs := make([]string, 0)
for priv, scope := range SCOPES {
if scope.Routine && !scope.Dynamic && priv != "USAGE" && priv != "GRANT OPTION" {
privs = append(privs, priv)
}
}
return privs
}
func checkIsRoutine(privs []string) bool {
if len(privs) > 0 {
return SCOPES[privs[0]].Routine
}
return false
}
func getTableIndex(d *Database, tableName string) int {
for i, t := range *d.Tables {
if t.Name == tableName {
return i
}
}
return -1
}
func getRoutineIndex(d *Database, routineName string) int {
for i, r := range *d.Routines {
if r.Name == routineName {
return i
}
}
return -1
}
func addRemovePrivs(currentPrivs []string, privsToAddRemove []string, add bool) []string {
newPrivs := make([]string, 0)
if add {
newPrivs = append(currentPrivs, privsToAddRemove...)
return newPrivs
}
for _, p := range currentPrivs {
found := false
for _, p2 := range privsToAddRemove {
if p == p2 {
found = true
break
}
}
if !found {
newPrivs = append(newPrivs, p)
}
}
return newPrivs
}
func printResults(databases map[string]*Database, globalPrivs GlobalPrivs, showAll bool) {
// Print Global Privileges
printGlobalPrivs(globalPrivs)
// Print Database and Table Privileges
printDBTablePrivs(databases, showAll)
// Print Routine Privileges
printRoutinePrivs(databases, showAll)
}
func printGlobalPrivs(globalPrivs GlobalPrivs) {
// Prep table writer
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Global Privileges"})
// Print global privs
globalPrivsStr := ""
for _, priv := range globalPrivs.Privs {
globalPrivsStr += priv + ", "
}
// Clean up privs string
globalPrivsStr = cleanPrivStr(globalPrivsStr)
// Add rows of priv string data
t.AppendRow([]interface{}{analyzers.GreenWriter(text.WrapSoft(globalPrivsStr, 100))})
t.Render()
}
func printDBTablePrivs(databases map[string]*Database, showAll bool) {
// Prep table writer
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Database", "Table", "Privileges", "Est. Size"})
// Print database privs
for _, d := range databases {
if isBuiltIn(d.Name) && !showAll {
continue
}
// Add privileges to db or table privs strings
dbPrivsStr := ""
dbTablesStr := ""
for _, priv := range d.Privs {
scope := SCOPES[priv]
if scope.Database && scope.Table {
dbTablesStr += priv + ", "
} else {
dbPrivsStr += priv + ", "
}
}
// Clean up privs strings
dbPrivsStr = cleanPrivStr(dbPrivsStr)
dbTablesStr = cleanPrivStr(dbTablesStr)
// Prep String colors
var dbName string
var writer func(a ...interface{}) string
if d.Default {
dbName = d.Name + " (built-in)"
writer = analyzers.YellowWriter
} else if d.Nonexistent {
dbName = d.Name + " (nonexistent)"
writer = analyzers.RedWriter
} else {
dbName = d.Name
writer = analyzers.GreenWriter
}
// Prep Priv Strings
// Add rows of priv string data
t.AppendRow([]interface{}{writer(dbName), writer("<DB-Level Privs>"), writer(text.WrapSoft(dbPrivsStr, 80)), writer("-")})
t.AppendRow([]interface{}{"", writer("<All tables>"), writer(text.WrapSoft(dbTablesStr, 80)), writer("-")})
// Print table privs
for _, t2 := range *d.Tables {
tablePrivsStr := ""
for _, priv := range t2.Privs {
tablePrivsStr += priv + ", "
}
tablePrivsStr = cleanPrivStr(tablePrivsStr)
t.AppendRow([]interface{}{"", writer(t2.Name), writer(text.WrapSoft(tablePrivsStr, 80)), writer(humanize.Bytes(t2.Bytes))})
}
// Add a separator between databases
t.AppendSeparator()
}
t.Render()
}
func printRoutinePrivs(databases map[string]*Database, showAll bool) {
// Print routine privs
t := table.NewWriter()
t.SetOutputMirror(os.Stdout)
t.AppendHeader(table.Row{"Database", "Routine", "Privileges"})
// Add rows of priv string data
for _, d := range databases {
if isBuiltIn(d.Name) && !showAll {
continue
}
for _, r := range *d.Routines {
routinePrivsStr := ""
for _, priv := range r.Privs {
routinePrivsStr += priv + ", "
}
routinePrivsStr = cleanPrivStr(routinePrivsStr)
var writer func(a ...interface{}) string
switch d.Name {
case mysql_db_info_sch, mysql_db_perf_sch, mysql_db_sys, mysql_db_mysql:
writer = analyzers.YellowWriter
default:
writer = analyzers.GreenWriter
}
t.AppendRow([]interface{}{writer(d.Name), writer(r.Name), writer(text.WrapSoft(routinePrivsStr, 80))})
}
}
t.Render()
}
func cleanPrivStr(priv string) string {
priv = strings.TrimSuffix(priv, ", ")
if priv == "" {
priv = "-"
}
return priv
}
func isBuiltIn(dbName string) bool {
switch dbName {
case mysql_db_sys, mysql_db_perf_sch, mysql_db_info_sch, mysql_db_mysql:
return true
}
return false
}