Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
liming6
sshd-tool
Commits
3abd184e
Commit
3abd184e
authored
Mar 24, 2026
by
liming6
Browse files
feature 添加scp监视功能
parent
4c295cf4
Changes
7
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
488 additions
and
229 deletions
+488
-229
Readme.md
Readme.md
+55
-17
build.sh
build.sh
+2
-5
cmd/file-monitor/logic/logic_test.go
cmd/file-monitor/logic/logic_test.go
+1
-1
cmd/file-monitor/logic/scp.go
cmd/file-monitor/logic/scp.go
+337
-130
cmd/file-monitor/logic/sftp.go
cmd/file-monitor/logic/sftp.go
+19
-4
cmd/file-monitor/main.go
cmd/file-monitor/main.go
+22
-66
cmd/file-monitor/readme.md
cmd/file-monitor/readme.md
+52
-6
No files found.
Readme.md
View file @
3abd184e
#
r
eadme
#
R
eadme
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
build.sh
View file @
3abd184e
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
cmd/file-monitor/logic/logic_test.go
View file @
3abd184e
...
...
@@ -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
)
}
...
...
cmd/file-monitor/logic/scp.go
View file @
3abd184e
...
...
@@ -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
}
cmd/file-monitor/logic/sftp.go
View file @
3abd184e
...
...
@@ -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
)
...
...
cmd/file-monitor/main.go
View file @
3abd184e
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
()
}
cmd/file-monitor/readme.md
View file @
3abd184e
#
r
eadme
#
R
eadme
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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment