Commit 3abd184e authored by liming6's avatar liming6
Browse files

feature 添加scp监视功能

parent 4c295cf4
# readme
# Readme
sshd-tool用于收集sshd的日志,过滤出用户的登录和退出ssh信息
file-minitor是一个监控sftp和scp上传文件动作并对上传文件进行病毒扫描的工具
依赖:
## 工作方式
- sshd
- rsyslog
(1) 针对sftp,它解析实时sftp日志,识别上传文件行为并使用clamdscan扫描文件
工作流程:
需要rsyslog和sshd服务同时启用
配置sshd和rsyslog,将sshd日志转发到机器上的一个unix socket或者远端的tcp或udp
a. 修改rsyslog配置,将日志发送到unix socket中
sshd-tool监听这个unix socket,过滤出需要的信息
```bash
# 写入配置文件
cat > /etc/rsyslog.d/sftp.conf <<EOF
\$ModLoad omuxsock
\$OMUxSockSocket /tmp/rsyslog.sock
authpriv.* :omuxsock:
EOF
除此之外,sshd-tool需要解析各个系统用户家目录下的.ssh/authorized_keys文件的内容
# 重启rsyslog服务,启用上述配置
systemctl restart rsyslog
```
最终,sshd-tool提供查询服务
b. 修改 sshd_config 配置,让sftp将日志写入rsyslog的AUTHPRIV事件队列中
## todo
```
# Subsystem sftp /usr/libexec/openssh/sftp-server
Subsystem sftp /usr/libexec/openssh/sftp-server -l INFO -f AUTHPRIV
```
重启sshd服务:`systemctl restart sshd`
经过上述配置后,即可在unix socket `/tmp/rsyslog.sock` 监听到sftp日志了
(2) 针对scp,它利用auditd中的审计规则判识别写文件动作,需要系统auditd服务,并添加以下审计规则:
```bash
# 监控scp创建文件动作
-a always,exit -F arch=b64 -S openat -F exe=/usr/bin/scp -F a2&0x40 -F key=scp_create_file
-a always,exit -F arch=b64 -S open -F exe=/usr/bin/scp -F a1&0x40 -F key=scp_create_file
# 监控scp关闭文件动作
-a always,exit -F arch=b64 -S close -F exe=/usr/bin/scp -F key=scp_close_file
```
可以将上述规则写到 `/etc/audit/rules.d/audit.rules` 中并通过 `service auditd reload`命令持久化
- 适配ubuntu系统,对于Ubuntu,系统who -u中的pid是ssh日志中pid的子进程,需要处理一下
- 能查询出以前的,没有被sshd-tool记录的在线情况
## 注意
一旦上传文件完成,file-monitor会立即修改文件名,添加 `.scanning` 后缀,若扫描到文件,删除文件,否则会恢复文件名
扫描病毒文件的时间会根据文件类型、文件大小而变化,最长扫描时间为5分钟
同样大小下,`.zip`文件扫描比较慢,建议压缩包格式为 `.tar.gz`
## 日志
日志会记录在 `/var/log/file-monitor.<启动时间>.log` 文件里
## todo
- sftp登录在sshd日志里有记录,而who -u的输出是没有记录的
- who -u的输出里,可能有多个pid相同的数据,那是同一个ssh连接的多个虚拟终端,由于没有登录动作,所以sshd日志里没有对应的日志条目
添加白名单:
## auditd日志分析
- 用户白名单:不扫描指定用户上传的文件
- 路径白名单:不扫描上传到指定路径的文件
增加可识别文件类型功能,对于文本类型,直接跳过扫描
针对压缩包,使用流式扫描以加速扫描
\ No newline at end of file
cd cmd/daemon
cd cmd/file-monitor
go clean
go build -ldflags="-s -w"
if type upx > /dev/null; then
upx daemon
fi
\ No newline at end of file
go build
\ No newline at end of file
......@@ -44,7 +44,7 @@ func TestScanFile(t *testing.T) {
}
func TestParseInt(t *testing.T) {
i, err := ParseInt("abc")
i, err := ParseInt("-321")
if err != nil {
t.Error(err)
}
......
......@@ -2,28 +2,24 @@ package logic
import (
"encoding/json"
"errors"
"fmt"
"log"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/alphadose/haxmap"
"github.com/elastic/go-libaudit/v2"
"github.com/elastic/go-libaudit/v2/aucoalesce"
"github.com/elastic/go-libaudit/v2/auparse"
"github.com/shirou/gopsutil/v4/process"
)
var (
// 接收审计事件的管道
EventChan = make(chan *aucoalesce.Event, 1024)
// 记录有效事件的map
EventMap = haxmap.New[string, []*TaggedEvent]()
// 记录需要注意的可执行文件路径
ExecPath = map[string]bool{
"/usr/bin/scp": true,
"/usr/libexec/openssh/sftp-server": true,
}
EventChan = make(chan *aucoalesce.Event, 8192)
// 记录需要注意的系统调用
Syscalls = map[string]FileAction{
......@@ -39,17 +35,12 @@ var (
"unlinkat": FA_Rename,
}
SCP = "/usr/bin/scp"
SFTP = "/usr/libexec/openssh/sftp-server"
)
SCP = "/usr/bin/scp"
// FileTransformExec 传输文件的程序类型
type FileTransformExec uint8
EventMap = make(map[string]*EventSet)
EventMapLock = sync.RWMutex{} // 保护EventMap的锁
const (
FTE_UNKNOWN FileTransformExec = iota
FTE_SFTP // sftp
FTE_SCP // scp
ErrArgNil = errors.New("error arg is nil")
)
type FileAction uint8
......@@ -61,92 +52,270 @@ const (
FA_Delete
)
type TaggedEvent struct {
Event *aucoalesce.Event
Exec FileTransformExec
IsTabby bool // 标记是否为tabby客户端
Fd *int // 记录文件描述符
func CleanSCP() {
EventMapLock.Lock()
defer EventMapLock.Unlock()
toDelete := make([]string, len(EventMap))
for k, v := range EventMap {
alive, _ := v.IsAlive()
if !alive {
toDelete = append(toDelete, k)
}
}
for _, v := range toDelete {
delete(EventMap, v)
}
}
func FromEvent(event *aucoalesce.Event) *TaggedEvent {
if event == nil {
return nil
}
result := TaggedEvent{
Event: event,
type EventHandler struct{}
func (h *EventHandler) ReassemblyComplete(msgs []*auparse.AuditMessage) {
event, err := aucoalesce.CoalesceMessages(msgs)
if err != nil {
log.Printf("coalesce messages error: %v \n", err)
}
switch event.Process.Exe {
case SCP:
result.Exec = FTE_SCP
case SFTP:
result.Exec = FTE_SFTP
default:
result.Exec = FTE_UNKNOWN
EventChan <- event
}
func (h *EventHandler) EventsLost(count int) {
log.Printf("event lost: %d \n", count)
}
func StartAuditMonitor(wg *sync.WaitGroup) {
defer wg.Done()
cli, err := libaudit.NewMulticastAuditClient(nil)
if err != nil {
log.Printf("failed to create audit client: %v \n", err)
return
}
// 如果是sftp,检查是否为
if result.Exec == FTE_SFTP {
defer cli.Close()
handler := &EventHandler{}
rea, err := libaudit.NewReassembler(8192, time.Second*180, handler)
if err != nil {
log.Printf("%v", err)
return
}
// 如果是open、openat、close,会有文件描述符
defer rea.Close()
// 定期维护事件池
go func() {
ticker := time.NewTicker(time.Second * 15)
defer ticker.Stop()
for range ticker.C {
if rea.Maintain() != nil {
break
}
}
}()
go FiltAuditMsg()
return &result
for {
rawMsg, err := cli.Receive(false)
if err != nil {
break
}
err = rea.Push(rawMsg.Type, slices.Clone(rawMsg.Data))
if err != nil {
log.Printf("push audit event error: %v\n", err)
}
}
close(EventChan)
}
// EventSet 事件集,一个事件集记录了同一个进程的审计事件
type EventSet struct {
Pid, PPID int // pid和ppid
Events []*aucoalesce.Event // 记录所有事件
Fds map[int64][]*aucoalesce.Event // 记录可以获取文件描述符的事件
IsTabby *bool // 记录是否为tabby客户端
Exec FileTransformExec // 记录文件传输工具
Lock sync.RWMutex // 锁,用于保护Fds和Events的读写
}
func NewEventSet() *EventSet {
return &EventSet{
Events: make([]*aucoalesce.Event, 0, 16),
Fds: make(map[int64][]*aucoalesce.Event),
IsTabby: nil,
Exec: FTE_UNKNOWN,
Lock: sync.RWMutex{},
}
}
// func (es *EventSet) AddEvent(e *aucoalesce.Event) error {
// if e == nil {
// return nil
// }
// es.Events = append(es.Events, e)
// sc := e.Data["syscall"]
// switch sc {
// case "close":
// case "openat":
// case "":
// }
// if sc == "openat" {
// i, err := ParseInt(e.Data["exit"])
// if err != nil {
// return nil
// }
// }
// return nil
// }
// GenKey 为事件生成字符串id,格式为 pid-ppid
func GenKey(event *aucoalesce.Event) string {
return fmt.Sprintf("%s-%s", event.Process.PID, event.Process.PPID)
Pid, PPID, Uid int32 // pid、ppid和Uid
Events []*aucoalesce.Event // 记录所有事件
Fds map[int64][]*aucoalesce.Event // 记录可以获取文件描述符的事件
Lock sync.RWMutex // 锁,用于保护Fds和Events的读写
}
// PushEvent 向事件集中插入事件,并判断是否需要触发文件扫描
func (es *EventSet) PushEvent(event *aucoalesce.Event, syscall string) {
if event == nil {
return
}
switch syscall {
case "open", "openat":
fd, err := getExitFd(event)
if err != nil {
log.Println("error: ", err)
return
}
if fd == nil {
log.Println("error: open(at) syscall, but can't get fd")
return
}
es.Lock.Lock()
el, have := es.Fds[*fd]
if have && len(el) > 0 {
// 已经用了这个fd,有问题,要么是没有删除旧fd,要么是有bug
bb, _ := json.Marshal(el)
cc, _ := json.Marshal(event)
log.Println("error: fd repeat, old: ", string(bb), "new: ", string(cc))
clear(el)
el = append(el, event)
es.Events = append(es.Events, event)
es.Lock.Unlock()
} else {
// ok,正常的
el = make([]*aucoalesce.Event, 0, 16)
el = append(el, event)
es.Fds[*fd] = el
es.Events = append(es.Events, event)
es.Lock.Unlock()
}
case "close":
// 获取close的第一个参数,那就是fd
if event.Data == nil {
return
}
fdStr, have := event.Data["a0"]
if !have {
log.Println("error: syscall close, but can't find the first arg")
return
}
fd, err := ParseInt(fdStr)
if err != nil {
log.Println("error: ", err)
return
}
es.Lock.Lock()
// 判断fd是否存在,存在就扫描文件
es.Events = append(es.Events, event)
el, have := es.Fds[fd]
if have {
delete(es.Fds, fd)
es.Lock.Unlock()
l := len(el)
if l == 0 {
// bug
log.Println("error: fds slices's length is 0")
return
}
filePath, err := getFilePath(el[l-1])
if err != nil {
log.Println("error: ", err)
return
}
if filePath == nil {
log.Println("error: find file path is nil")
return
}
go es.ScanFile(*filePath)
} else {
// 不存在就返回
es.Events = append(es.Events, event)
es.Lock.Unlock()
}
}
}
func (es *EventSet) ScanFile(path string) {
log.Printf("user UID(%d) upload file: %s, scanning...\n", es.Uid, path)
v, _ := ScanFile(path)
if v {
log.Printf("user UID(%d) upload file %s containing viruses \n", es.Uid, path)
} else {
log.Printf("user UID(%d) upload file %s not find virus \n", es.Uid, path)
}
}
// NewEventSet 从open、openat系统调用创建事件集
func NewEventSet(event *aucoalesce.Event, _ string) (*EventSet, error) {
info, err := getBaseInfo(event)
if err != nil {
return nil, err
}
result := EventSet{
Events: make([]*aucoalesce.Event, 0, 16),
Fds: make(map[int64][]*aucoalesce.Event),
Lock: sync.RWMutex{},
}
result.Events = append(result.Events, event)
if info[0] != nil {
result.Pid = *info[0]
}
if info[1] != nil {
result.PPID = *info[1]
}
if info[2] != nil {
result.Uid = *info[2]
}
if event.Data != nil {
exit, have := event.Data["exit"]
if !have {
return nil, errors.New("create file syscall, but not find exit code")
}
fd, err := ParseInt(exit)
if err != nil {
return nil, err
}
el := make([]*aucoalesce.Event, 0, 16)
el = append(el, event)
result.Fds[fd] = el
}
return &result, nil
}
// CheckAlive 检查进程是否存活
func (es *EventSet) IsAlive() (bool, error) {
p, err := process.NewProcess(es.Pid)
if err != nil {
// 进程已经结束
return false, nil
}
ppid, err := p.Ppid()
if err != nil {
return false, nil
}
if ppid == es.PPID {
return true, nil
}
return false, nil
}
// FiltMsg 过滤出有用的事件,存放到map中,并触发处理程序
func FiltMsg() {
// PushEvent 向EventMap中放入事件
func PushEvent(syscall string, event *aucoalesce.Event) {
if event == nil {
return
}
key := genKey(event)
switch syscall {
case "open", "openat":
// 新建文件
EventMapLock.Lock()
es, have := EventMap[key]
if !have {
ies, err := NewEventSet(event, syscall)
if err != nil {
log.Println(err)
EventMapLock.Unlock()
return
}
es = ies
EventMap[key] = es
}
EventMapLock.Unlock()
case "close":
// 关闭文件
EventMapLock.Lock()
es, have := EventMap[key]
if !have {
EventMapLock.Unlock()
return
}
EventMapLock.Unlock()
es.PushEvent(event, syscall)
}
}
// FiltAuditMsg 过滤出有用的事件,存放到map中,并触发处理程序
func FiltAuditMsg() {
for i := range EventChan {
v, have := ExecPath[i.Process.Exe]
if !(v && have) {
// 不是指定的exe跳过
if i.Process.Exe != SCP {
continue
}
if i.Category != aucoalesce.EventTypeAuditRule {
......@@ -157,10 +326,6 @@ func FiltMsg() {
// 不是系统调用,跳过
continue
}
// if i.Data == nil {
// // Data里没有数据的跳过
// continue
// }
sc, have := i.Data["syscall"]
if !have {
continue
......@@ -174,50 +339,29 @@ func FiltMsg() {
if i.Result != "success" {
continue
}
printEvent(i)
// printEvent(i)
PushEvent(sc, i)
}
}
// HandleEvent 处理事件
func HaneldEvent(events []*TaggedEvent) {
// todo
panic("unimplemented")
}
// needHandle 判断是否需要进入处理流程
func needHandle(events []*TaggedEvent) bool {
// todo
panic("unimplemented")
}
func printEvent(e *aucoalesce.Event) {
// pid,ppid,syscall,
// open/openat:file,fd
// close:fd
if e == nil {
return
}
fmt.Println("-----")
switch e.Data["syscall"] {
case "open", "openat":
fmt.Printf("open(at) pid: %s, ppid: %s, open %s, fd: %s \n", e.Process.PID, e.Process.PPID, e.File.Path, e.Data["exit"])
fd, _ := getExitFd(e)
fp, _ := getFilePath(e)
log.Printf("open(at): pid: %s ,fd: %d, file: %s \n", e.Process.PID, *fd, *fp)
case "close":
fmt.Printf("close pid: %s, ppid: %s, fd: %s \n", e.Process.PID, e.Process.PPID, e.Data["a0"])
case "rename", "renameat", "renameat2":
fmt.Println(json.MarshalIndent(e, "", " "))
case "link", "linkat":
fmt.Printf("link(at): paths: %+v\n", e.Paths)
case "unlink", "unlinkat":
fmt.Printf("unlink(at): paths: %+v\n", e.Paths)
case "dup2", "dup3":
fmt.Println("dup")
log.Printf("close: pid: %s ,fd: %s \n", e.Process.PID, e.Data["a0"])
default:
return
log.Println("unknown syscall: ", e.Data["syscall"])
}
// bb, err := json.MarshalIndent(e, "", " ")
// if err == nil {
// fmt.Println(string(bb))
// }
}
func ParseInt(str string) (int64, error) {
......@@ -231,10 +375,73 @@ func ParseInt(str string) (int64, error) {
}
} else {
// 不是16进制数
n, err := strconv.Atoi(str)
n, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return 0, err
}
return int64(n), err
return n, err
}
}
// genKey 为事件生成字符串id,格式为 pid-ppid
func genKey(event *aucoalesce.Event) string {
return fmt.Sprintf("%s-%s", event.Process.PID, event.Process.PPID)
}
// getBaseInfo 获取事件的基本信息,pid、ppid、uid
func getBaseInfo(event *aucoalesce.Event) ([3]*int32, error) {
result := [3]*int32{nil, nil, nil}
if event == nil {
return result, ErrArgNil
}
var pid, ppid, uid int32
if event.User.IDs != nil {
u, have := event.User.IDs["uid"]
if have {
uu, err := ParseInt(u)
if err == nil {
uid = int32(uu)
result[2] = &uid
}
}
}
p, err := ParseInt(event.Process.PID)
if err == nil {
pid = int32(p)
result[0] = &pid
}
p, err = ParseInt(event.Process.PPID)
if err == nil {
ppid = int32(p)
result[0] = &ppid
}
return result, nil
}
func getFilePath(event *aucoalesce.Event) (*string, error) {
if event == nil {
return nil, ErrArgNil
}
if event.File == nil {
return nil, nil
}
return &event.File.Path, nil
}
// getExitFd 获取系统调用的返回值
func getExitFd(event *aucoalesce.Event) (*int64, error) {
if event == nil {
return nil, ErrArgNil
}
if event.Data != nil {
exit, have := event.Data["exit"]
if have {
e, err := ParseInt(exit)
if err != nil {
return nil, err
}
return &e, nil
}
}
return nil, nil
}
......@@ -30,6 +30,21 @@ var (
RegForceCloseFile = regexp.MustCompile(`(?i)^forced close "(.*)" bytes read (\d+) written (\d+)$`)
)
func CleanSftp() {
SftpLogLock.Lock()
defer SftpLogLock.Unlock()
toDelete := make([]int32, len(SftpLogMap))
for k, v := range SftpLogMap {
alive, _ := v.CheckAlive()
if !alive {
toDelete = append(toDelete, k)
}
}
for _, v := range toDelete {
delete(SftpLogMap, v)
}
}
type GetSLA interface {
GetFfileAction() SftpLogAction
SetPid(pid int32)
......@@ -454,9 +469,9 @@ func InsertAction(action GetSLA) {
finfo.LogLock.Unlock()
} else {
// 发生重复了???
log.Println("warn: sftp file path repeat")
ls.Lock.Unlock()
}
return
case *SftpLogClose:
if num, have := act.Write.Get(); have && num == 0 {
......@@ -553,7 +568,6 @@ func InsertAction(action GetSLA) {
}
}
// todo 需要适应文件名有空格的情况
func ParseSftpLog(s string) (GetSLA, error) {
if !strings.Contains(s, "sftp-server") {
// 不是sftp日志
......@@ -594,7 +608,8 @@ func ParseSftpLog(s string) (GetSLA, error) {
return fta, nil
}
func StartSftpMonitor() {
func StartSftpMonitor(wg *sync.WaitGroup) {
defer wg.Done()
regRemove := regexp.MustCompile(`^<\d+>(.*)$`)
os.Remove("/tmp/rsyslog.sock")
conn, err := net.ListenPacket("unixgram", "/tmp/rsyslog.sock")
......@@ -614,7 +629,7 @@ func StartSftpMonitor() {
continue
}
if strings.Contains(items[1], "sftp-server") {
fmt.Println(items[1])
// fmt.Println(items[1])
l, err := ParseSftpLog(items[1])
if err != nil {
log.Println(err)
......
package main
import (
"context"
"fmt"
"log"
"os"
"sshd-tool/cmd/file-monitor/logic"
"sync"
"time"
"github.com/gofrs/flock"
"github.com/spf13/pflag"
)
// import (
// "fmt"
// "log"
// "os"
// "sshd-tool/cmd/file-monitor/logic"
// "time"
// "github.com/elastic/go-libaudit/v2"
// "github.com/elastic/go-libaudit/v2/aucoalesce"
// "github.com/elastic/go-libaudit/v2/auparse"
// )
// type EventHandler struct{}
// func (h *EventHandler) ReassemblyComplete(msgs []*auparse.AuditMessage) {
// event, err := aucoalesce.CoalesceMessages(msgs)
// if err != nil {
// fmt.Printf("coalesce messages error: %v", err)
// }
// logic.EventChan <- event
// }
// func (h *EventHandler) EventsLost(count int) {
// fmt.Fprintf(os.Stderr, "=== event lost: %d \n", count)
// }
// func main() {
// cli, err := libaudit.NewMulticastAuditClient(nil)
// if err != nil {
// log.Fatalf("failed to create audit client: %v", err)
// }
// defer cli.Close()
// handler := &EventHandler{}
// rea, err := libaudit.NewReassembler(1024, time.Second*60, handler)
// if err != nil {
// log.Printf("%v", err)
// return
// }
// defer rea.Close()
// go func() {
// ticker := time.NewTicker(time.Second * 15)
// defer ticker.Stop()
// for range ticker.C {
// if rea.Maintain() != nil {
// break
// }
// }
// }()
// go logic.FiltMsg()
// for {
// rawMsg, err := cli.Receive(false)
// if err != nil {
// break
// }
// _ = rea.Push(rawMsg.Type, rawMsg.Data)
// }
// close(logic.EventChan)
// }
var (
logfile *os.File
flagDebug = pflag.Bool("debug", false, "debug mode, print log to stdout, not file")
......@@ -109,6 +47,24 @@ func main() {
defer logFile.Close()
}
}
logic.StartSftpMonitor()
wg := sync.WaitGroup{}
wg.Add(2)
go logic.StartSftpMonitor(&wg)
go logic.StartAuditMonitor(&wg)
ctx, cancel := context.WithCancel(context.Background())
go func(c context.Context) {
ticker := time.NewTicker(time.Minute * 5)
defer ticker.Stop()
for {
select {
case <-ticker.C:
logic.CleanSCP()
logic.CleanSftp()
case <-c.Done():
return
}
}
}(ctx)
wg.Wait()
cancel()
}
# readme
# Readme
file-minitor是一个监控sftp和scp上传文件动作并对上传文件进行病毒扫描的工具
## 工作方式
针对sftp,它解析实时sftp日志,识别上传文件行为并使用clamdscan扫描文件
(1) 针对sftp,它解析实时sftp日志,识别上传文件行为并使用clamdscan扫描文件
针对scp,它利用auditd中的审计规则判识别写文件动作
需要rsyslog和sshd服务同时启用
a. 修改rsyslog配置,将日志发送到unix socket中
```bash
# 写入配置文件
cat > /etc/rsyslog.d/sftp.conf <<EOF
\$ModLoad omuxsock
\$OMUxSockSocket /tmp/rsyslog.sock
authpriv.* :omuxsock:
EOF
# 重启rsyslog服务,启用上述配置
systemctl restart rsyslog
```
b. 修改 sshd_config 配置,让sftp将日志写入rsyslog的AUTHPRIV事件队列中
```
# Subsystem sftp /usr/libexec/openssh/sftp-server
Subsystem sftp /usr/libexec/openssh/sftp-server -l INFO -f AUTHPRIV
```
重启sshd服务:`systemctl restart sshd`
经过上述配置后,即可在unix socket `/tmp/rsyslog.sock` 监听到sftp日志了
(2) 针对scp,它利用auditd中的审计规则判识别写文件动作,需要系统auditd服务,并添加以下审计规则:
```bash
# 监控scp创建文件动作
-a always,exit -F arch=b64 -S openat -F exe=/usr/bin/scp -F a2&0x40 -F key=scp_create_file
-a always,exit -F arch=b64 -S open -F exe=/usr/bin/scp -F a1&0x40 -F key=scp_create_file
# 监控scp关闭文件动作
-a always,exit -F arch=b64 -S close -F exe=/usr/bin/scp -F key=scp_close_file
```
可以将上述规则写到 `/etc/audit/rules.d/audit.rules` 中并通过 `service auditd reload`命令持久化
一旦上传文件完成,file-monitor会立即修改文件名,添加 `.scanning` 后缀,若扫描到文件,删除文件,否则会恢复文件名
一旦上传文件完成,file-monitor会立即修改文件名,添加 `.scanning` 后缀,若扫描到文件,删除文件,并在原地留下一个警示文件 `.virus`
如果没有病毒,会恢复文件名
扫描病毒文件的时间会根据文件类型、文件大小而变化,最长扫描时间为5分钟
同样大小下,`.zip`文件扫描比较慢,建议压缩包格式为 `.tar.gz`
## 日志
日志会记录在 `/var/log/file-monitor.<启动时间>.log` 文件里
## todo
添加白名单:
- 用户白名单:不扫描指定用户上传的文件
- 路径白名单:不扫描上传到指定路径的文件
\ No newline at end of file
- 路径白名单:不扫描上传到指定路径的文件
增加可识别文件类型功能,对于文本类型,直接跳过扫描
针对压缩包,使用流式扫描以加速扫描
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment