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
}