From 1b59f9e137e90db413250f1cdd9683feb468665f Mon Sep 17 00:00:00 2001 From: Niko Diamadis Date: Fri, 20 Dec 2024 19:53:42 -0500 Subject: [PATCH] feat: add MIME type filter --- .env_template | 1 + README.md | 18 +++++++++-- config.go | 1 + db/database.go | 45 ++++++++++++++++++++++++++ docker-compose-dev.yml | 1 + docker-compose.yml | 1 + help.go | 11 +++++++ main.go | 71 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 146 insertions(+), 3 deletions(-) diff --git a/.env_template b/.env_template index 8fcdab0..05a56f2 100644 --- a/.env_template +++ b/.env_template @@ -9,3 +9,4 @@ GUARDIAN_VIRUS_TOTAL_KEY= GUARDIAN_URL_FILTER=true GUARDIAN_URL_CHECK_VIRUS_TOTAL=false GUARDIAN_URL_CHECK_FISHFISH=false +GUARDIAN_MIME_FILTER=true diff --git a/README.md b/README.md index f16a645..1fa7f83 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ - [URL Phishing Check 🗡️](#url-phishing-check-) - [VirusTotal](#virustotal) - [FishFish](#fishfish) - - [planned] *File Type Filter* 📎 +a - [File MIME Type Filter 📎](#file-mime-type-filter-) - [planned] *File Virus Scan* 🦠 - [planned] *Keyword Filter* 📄 - [Protected Public Rooms (Mentions)](#protected-public-rooms-mentions) @@ -27,8 +27,8 @@ Guardian supports URL filtering based on a customizable domain list. **Examples**: -- `!gd block t.me` -- `!gd unblock t.me` +- `!gd url block t.me` +- `!gd url unblock t.me` ### URL Phishing Check 🗡 @@ -52,6 +52,18 @@ Guardian rates a URL "suspicious" if the statistics `malicious` and `suspicious` FishFish allows scanning a domain and returning a rating, if found in their reports. Guardian rates a URL "suspicious" if the FishFish rating is `malware` or `phishing` rather than `safe`. +### File MIME Type Filter 📎 + +**Activation (default: true)**: `GUARDIAN_MIME_FILTER: true|false` +**Help Command**: `!gd mime` + +Guardian supports file MIME type filtering based on a customizable MIME type list. + +**Examples**: +- `!gd mime block application/zip` +- `!gd mime unblock application/zip` +- `!gd mime list` + ## Protected Public Rooms (Mentions) This list showcases some of the rooms who use the Matrix Guardian 🛡️: diff --git a/config.go b/config.go index 8c624e8..d76b2dc 100644 --- a/config.go +++ b/config.go @@ -23,6 +23,7 @@ type Config struct { useUrlFilter bool // "GUARDIAN_URL_FILTER", default: true useUrlCheckVt bool // "GUARDIAN_URL_CHECK_VIRUS_TOTAL", default: false useUrlCheckFf bool // "GUARDIAN_URL_CHECK_FISHFISH", default: false + useMimeFilter bool // "GUARDIAN_MIME_FILTER", default: true } func CheckForDefaultConfig(username string, password string) { diff --git a/db/database.go b/db/database.go index 6da7dcd..2c25a9c 100644 --- a/db/database.go +++ b/db/database.go @@ -2,6 +2,7 @@ package db import ( "database/sql" + "fmt" _ "github.com/mattn/go-sqlite3" ) @@ -9,8 +10,14 @@ type table struct { name, values string } +type mimetype struct { + name string + count int +} + var tables = []table{ {"domains", "name TEXT PRIMARY KEY, count INT"}, + {"mimetypes", "name TEXT PRIMARY KEY, count INT"}, {"attributes", "key TEXT PRIMARY KEY, value TEXT"}, } @@ -40,6 +47,20 @@ func IsDomainBlocked(db *sql.DB, domain string) bool { return true } +func IsMimeBlocked(db *sql.DB, mime string) bool { + query := db.QueryRow("SELECT count FROM mimetypes WHERE name = ?", mime) + var count int + err := query.Scan(&count) + if err != nil { + // not found in database, implicitly allowed + return false + } + // update usage counter + _, _ = db.Exec("UPDATE mimetypes SET count = ? WHERE name = ?", count+1, mime) + // found in database, explicitly blocked + return true +} + func BlockDomain(db *sql.DB, domain string) bool { _, err := db.Exec("INSERT INTO domains (name, count) values (?, 0)", domain) return err == nil @@ -50,6 +71,30 @@ func UnblockDomain(db *sql.DB, domain string) bool { return err == nil } +func BlockMime(db *sql.DB, mime string) bool { + _, err := db.Exec("INSERT INTO mimetypes (name, count) values (?, 0)", mime) + return err == nil +} + +func UnblockMime(db *sql.DB, mime string) bool { + _, err := db.Exec("DELETE FROM mimetypes WHERE name = ?", mime) + return err == nil +} + +func ListMimes(db *sql.DB) ([]string, error) { + query, err := db.Query("SELECT name, count FROM mimetypes ORDER BY count DESC") + if err != nil { + return nil, err + } + var rows []string + for query.Next() { + var row mimetype + _ = query.Scan(&row.name, &row.count) + rows = append(rows, fmt.Sprintf("- %s (%d)", row.name, row.count)) + } + return rows, nil +} + func createAllTables(db *sql.DB) { for _, tab := range tables { createTable(db, tab.name, tab.values) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 4d49226..3d95e66 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -19,6 +19,7 @@ services: GUARDIAN_URL_FILTER: ${GUARDIAN_URL_FILTER} GUARDIAN_URL_CHECK_VIRUS_TOTAL: ${GUARDIAN_URL_CHECK_VIRUS_TOTAL} GUARDIAN_URL_CHECK_FISHFISH: ${GUARDIAN_URL_CHECK_FISHFISH} + GUARDIAN_MIME_FILTER: ${GUARDIAN_MIME_FILTER} volumes: guardian-data: diff --git a/docker-compose.yml b/docker-compose.yml index 796577b..dcf2706 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,7 @@ services: GUARDIAN_URL_FILTER: true GUARDIAN_URL_CHECK_VIRUS_TOTAL: false GUARDIAN_URL_CHECK_FISHFISH: false + GUARDIAN_MIME_FILTER: true volumes: guardian-data: diff --git a/help.go b/help.go index f8b473a..90ee89c 100644 --- a/help.go +++ b/help.go @@ -19,6 +19,13 @@ const urlHelp = "🛡️ Guardian Help Page [url] 🛡️:
" + "block <>: Block domain in messages
" + "unblock <>: Unblock domain in messages" +const mimeHelp = "🛡️ Guardian Help Page [mime] 🛡️:
" + + "!gd mime <>

" + + "Arguments:
" + + "block <>: Block MIME type in messages
" + + "unblock <>: Unblock MIME type in messages
" + + "list: List blocked MIME type in messages" + func getRawMessage(source string) string { source = strings.ReplaceAll(source, "", "") source = strings.ReplaceAll(source, "", "") @@ -59,3 +66,7 @@ func ShowHelp(client *mautrix.Client, ctx context.Context, mngtRoomId id.RoomID) func ShowUrlHelp(client *mautrix.Client, ctx context.Context, mngtRoomId id.RoomID) { sendHtmlMessage(client, ctx, mngtRoomId, urlHelp) } + +func ShowMimeHelp(client *mautrix.Client, ctx context.Context, mngtRoomId id.RoomID) { + sendHtmlMessage(client, ctx, mngtRoomId, mimeHelp) +} diff --git a/main.go b/main.go index 44640b6..a7a69b1 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,7 @@ import ( "maunium.net/go/mautrix/id" "os" "regexp" + "strings" "time" ) @@ -132,6 +133,38 @@ func onManagementMessage(client *mautrix.Client, ctx context.Context, evt *event } } ShowUrlHelp(client, ctx, config.mngtRoomId) + case "mime": + if len(subcommands) > 0 { + switch subcommands[0] { + case "block": + if len(subcommands) == 2 { + db.BlockMime(database, subcommands[1]) + return + } + case "unblock": + if len(subcommands) == 2 { + db.UnblockMime(database, subcommands[1]) + return + } + case "list": + if len(subcommands) == 1 { + list, err := db.ListMimes(database) + if err != nil { + return + } + message := fmt.Sprintf( + "Configured MIME types to block:\n%s", + strings.Join(list, "\n"), + ) + _, err = client.SendNotice(ctx, evt.RoomID, message) + if err != nil { + return + } + return + } + } + } + ShowMimeHelp(client, ctx, config.mngtRoomId) default: ShowHelp(client, ctx, config.mngtRoomId) } @@ -140,6 +173,8 @@ func onManagementMessage(client *mautrix.Client, ctx context.Context, evt *event func onProtectedRoomMessage(client *mautrix.Client, ctx context.Context, evt *event.Event) { const keyMessageType = "msgtype" + const keyInfo = "info" + const keyMimetype = "mimetype" contentJson, err := json.Marshal(evt.Content.Parsed) var contentParsed map[string]interface{} @@ -149,6 +184,7 @@ func onProtectedRoomMessage(client *mautrix.Client, ctx context.Context, evt *ev } messageType := contentParsed[keyMessageType] if messageType == "m.text" || messageType == "m.notice" || messageType == "m.emote" { + // text message if !config.useUrlFilter && !config.useUrlCheckVt && !config.useUrlCheckFf { return } @@ -182,6 +218,35 @@ func onProtectedRoomMessage(client *mautrix.Client, ctx context.Context, evt *ev if err != nil { return } + } else { + // file message + if !config.useMimeFilter { // TODO check for file scan activation + return + } + messageInfo := contentParsed[keyInfo] + if messageInfo == nil { + return + } + mimetype := messageInfo.(map[string]interface{})[keyMimetype].(string) + if mimetype == "" { + return + } + if config.useMimeFilter && db.IsMimeBlocked(database, mimetype) { + redactMessage(client, ctx, evt, "found blocklisted mimetype") + return + } + if config.hiddenMode { + return + } + if strings.HasPrefix(mimetype, "image/") { + // don't show reaction in hidden mode and for images + _ = client.SendReceipt(ctx, evt.RoomID, evt.ID, event.ReceiptTypeRead, nil) + return + } + _, err := client.SendReaction(ctx, evt.RoomID, evt.ID, "🛡️") + if err != nil { + return + } } } @@ -272,12 +337,14 @@ func readConfig() Config { useUrlFilter := util.GetEnv("GUARDIAN_URL_FILTER", true, true) useUrlCheckVt := util.GetEnv("GUARDIAN_URL_CHECK_VIRUS_TOTAL", true, true) useUrlCheckFf := util.GetEnv("GUARDIAN_URL_CHECK_FISHFISH", true, true) + useMimeFilter := util.GetEnv("GUARDIAN_MIME_FILTER", true, true) mngtRoomReportsBool := true testModeBool := false hiddenModeBool := false useUrlFilterBool := true useUrlCheckVtBool := false useUrlCheckFfBool := false + useMimeFilterBool := true // REQUIRED // if !validation.IsValidUrl(homeserver) { @@ -327,6 +394,9 @@ func readConfig() Config { if useUrlCheckFf == "true" { useUrlCheckFfBool = true } + if useMimeFilter == "false" { + useMimeFilterBool = false + } config = Config{ // REQUIRED // @@ -342,6 +412,7 @@ func readConfig() Config { useUrlFilter: useUrlFilterBool, useUrlCheckVt: useUrlCheckVtBool, useUrlCheckFf: useUrlCheckFfBool, + useMimeFilter: useMimeFilterBool, } return config }