# 通用前置约定 & 规则重申
## 一、核心规则(最终版)
1. **合法新增后缀(仅这类新增不告警)**
允许:`.jpg` `.png`
其余后缀文件**只要新增必告警**。
2. **全局文件白名单**
白名单文件:记录加入瞬间 MD5 状态,**仅监测内容修改**,无修改不展示,有修改强制告警。
3. **CMS 缓存 / 日志 / Session 目录**
内置主流 CMS 临时目录,**扫描全程直接跳过**,不参与遍历、比对,杜绝无效告警。
4. **PHP 文件交互**
列出所有 PHP 并编号 → 选择编号加入白名单 → 剩余 PHP 自选:全删 / 部分删 / 保留。
5. **日志**
全流程操作、变更、删除、加白、异常全部落地日志,可审计溯源。
6. **状态文件锁策略**
- 基准库 `base.db`:首次生成后 `chattr +i` / 只读锁定,永久不变;误删则重建。
- 动态库 `dynamic.db`:每次更新前解锁、写入完毕立即重新上锁。
7. **断点续扫**
超大目录生成**进度临时文件**,中断后下次执行继续扫描,防止重头遍历卡顿。
8. **白名单校验**
自动过滤空行、空格,校验路径有效性、重复项,异常输出提示 + 日志记录。
9. **运行身份**
Linux 必须 `root`;Windows 必须 **管理员身份** 运行。
## 二、路径统一配置
### Linux
- 网站根目录:`/www`
- 基准库:`/usr/local/www_base.db`(永久锁定)
- 动态库:`/usr/local/www_dynamic.db`(读写自动加解锁)
- 白名单:`/usr/local/www_whitelist.txt`
- 运行日志:`/var/log/www_file_check.log`
- 断点进度文件:`/tmp/www_scan_progress.tmp`
- CMS 排除目录:内置脚本内
### Windows
- 网站根目录:`D:\www`
- 基准库:`D:\script\www_base.db`(永久只读)
- 动态库:`D:\script\www_dynamic.db`(读写自动加解锁)
- 白名单:`D:\script\www_whitelist.txt`
- 运行日志:`D:\script\www_file_check.log`
- 断点进度文件:`D:\script\www_scan_progress.tmp`
- CMS 排除目录:内置脚本内
---
# 第一部分:Linux 版本(Shell 脚本)
## 1. 前置初始化(仅首次执行前操作)
```
- # 创建目录 & 空白文件
- mkdir -p /usr/local /var/log /tmp
- touch /usr/local/www_whitelist.txt
- chmod 700 /usr/local/www_check.sh
复制代码
```
## 2. 完整脚本 `/usr/local/www_check.sh`
```
```
## 3. Linux 使用方法
1. 赋予执行权限
```
- chmod +x /usr/local/www_check.sh
复制代码
```
2. **首次执行**(生成基准库 + 锁定)
```
```
3. **日常手动执行**
```
```
4. 编辑白名单(一行一个完整路径)
```
- vi /usr/local/www_whitelist.txt
复制代码
```
5. 临时解锁基准库(仅维护)
```
- chattr -i /usr/local/www_base.db
复制代码
```
---
# 第二部分:Windows 版本(PowerShell 脚本)
## 1. 前置准备
1. 创建目录:`D:\script`
2. 新建空白文件:`D:\script\www_whitelist.txt`
3. **必须右键 PowerShell → 以管理员身份运行**
## 2. 完整脚本 `D:\script\www_check.ps1`
```
- <#
- Windows 网站文件安全检测脚本
- 运行要求:管理员身份启动PowerShell,手动执行
- #>
- # ====================== 基础配置 ======================
- $WWW_DIR = "D:\www"
- $BASE_DB = "D:\script\www_base.db"
- $DYNAMIC_DB = "D:\script\www_dynamic.db"
- $WHITELIST = "D:\script\www_whitelist.txt"
- $LOG_FILE = "D:\script\www_file_check.log"
- $PROGRESS_FILE = "D:\script\www_scan_progress.tmp"
- $Now = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
- # 合法新增后缀
- $ALLOW_EXT = @("jpg","png")
- # CMS 全局排除目录
- $EXCLUDE_DIRS = @(
- "\data\cache","\data\log","\data\session","\data\sessions","\temp","\tmp","\cache",
- "\runtime","\logs","\session","\var\cache","\var\log","\attachments\cache",
- "\templets\cache","\e\data\cache","\e\data\log","\e\tmp",
- "\wp-content\cache","\wp-content\uploads\cache",
- "\data\threadcache","\dayrui\cache","\dayrui\log","\dayrui\tmp",
- "\zb_users\cache","\zb_users\log",
- "\runtime\cache","\runtime\log"
- )
- # ====================== 公共函数 ======================
- # 写入日志
- function Write-Log{
- param([string]$Msg)
- $logStr = "[$Now] $Msg"
- Add-Content -Path $LOG_FILE -Value $logStr -Encoding UTF8
- Write-Host $logStr
- }
- # 设置只读锁定
- function Lock-File{
- param([string]$Path)
- if(Test-Path $Path){
- $attr = (Get-Item $Path).Attributes
- Set-ItemProperty -Path $Path -Name Attributes -Value ($attr + [System.IO.FileAttributes]::ReadOnly)
- }
- }
- # 解除只读
- function Unlock-File{
- param([string]$Path)
- if(Test-Path $Path){
- Set-ItemProperty -Path $Path -Name Attributes -Value [System.IO.FileAttributes]::Normal
- }
- }
- # 白名单校验
- function Test-WhiteList{
- Write-Log "开始校验白名单文件 $WHITELIST"
- $lines = Get-Content $WHITELIST -Encoding UTF8 -ErrorAction SilentlyContinue
- $outLines = @()
- $pathDict = @{}
- foreach($line in $lines){
- $line = $line.Trim()
- if([string]::IsNullOrEmpty($line)){ continue }
- if($pathDict.ContainsKey($line)){
- Write-Log "WARN: 白名单重复路径: $line"
- continue
- }
- $pathDict[$line] = $true
- if(-not (Test-Path $line -PathType Leaf)){
- Write-Log "WARN: 白名单路径不存在: $line"
- continue
- }
- $outLines += $line
- }
- $outLines | Out-File $WHITELIST -Encoding UTF8 -Force
- Write-Log "白名单校验完成"
- }
- # 生成快照 + 断点续扫
- function New-Snapshot{
- param([string]$DbPath)
- Unlock-File $DbPath
- "" | Out-File $DbPath -Encoding UTF8 -Force
- $excludePaths = $EXCLUDE_DIRS | ForEach-Object { "$WWW_DIR$_" }
- $lastPath = ""
- if(Test-Path $PROGRESS_FILE){
- $lastPath = Get-Content $PROGRESS_FILE -Encoding UTF8
- Write-Log "检测到断点进度,从 $lastPath 继续扫描"
- }
- $files = Get-ChildItem -Path $WWW_DIR -Recurse -File -ErrorAction SilentlyContinue | Where-Object {
- $fp = $_.FullName
- $isExclude = $false
- foreach($ep in $excludePaths){
- if($fp.StartsWith($ep)){ $isExclude = $true; break }
- }
- -not $isExclude
- }
- $start = [string]::IsNullOrEmpty($lastPath)
- foreach($f in $files){
- if(-not $start){
- if($f.FullName -eq $lastPath){ $start = $true }
- continue
- }
- $f.FullName | Out-File $PROGRESS_FILE -Encoding UTF8 -Force
- $md5 = (Get-FileHash -Path $f.FullName -Algorithm MD5).Hash
- "$md5|$($f.FullName)" | Out-File $DbPath -Append -Encoding UTF8
- }
- Remove-Item $PROGRESS_FILE -Force -ErrorAction SilentlyContinue
- Lock-File $DbPath
- Write-Log "快照 $DbPath 生成完成并已锁定"
- }
- # 快照比对
- function Compare-Snap{
- param([string]$OldDb,[string]$NewDb)
- $oldData = Get-Content $OldDb -Encoding UTF8
- $oldHash = @{}
- foreach($line in $oldData){
- $arr = $line -split "\|",2
- $oldHash[$arr[1]] = $arr[0]
- }
- $newFiles = @()
- $modFiles = @()
- $newData = Get-Content $NewDb -Encoding UTF8
- foreach($line in $newData){
- $arr = $line -split "\|",2
- $p = $arr[1]
- $h = $arr[0]
- if(-not $oldHash.ContainsKey($p)){
- $ext = $p.Split(".")[-1].ToLower()
- $allow = $false
- foreach($ae in $ALLOW_EXT){
- if($ext -eq $ae){ $allow = $true; break }
- }
- if(-not $allow){ $newFiles += $p }
- }
- else{
- if($oldHash[$p] -ne $h){ $modFiles += $p }
- }
- }
- if($newFiles.Count -gt 0){
- Write-Log "===== 检测到【非法新增文件】 ====="
- $newFiles | ForEach-Object { Write-Log $_ }
- }
- else{ Write-Log "未检测到非法新增文件" }
- if($modFiles.Count -gt 0){
- Write-Log "===== 检测到【内容修改文件】 ====="
- $modFiles | ForEach-Object { Write-Log $_ }
- }
- else{ Write-Log "未检测到内容修改文件" }
- }
- # 白名单修改检测
- function Check-WhiteModify{
- Write-Log "===== 白名单文件修改检测 ====="
- if(-not (Test-Path $WHITELIST) -or (Get-Content $WHITELIST -Encoding UTF8).Count -eq 0){
- Write-Log "白名单为空,跳过检测"
- return
- }
- $baseData = Get-Content $BASE_DB -Encoding UTF8
- $baseHash = @{}
- foreach($line in $baseData){
- $arr = $line -split "\|",2
- $baseHash[$arr[1]] = $arr[0]
- }
- $whitePaths = Get-Content $WHITELIST -Encoding UTF8
- foreach($p in $whitePaths){
- if(-not (Test-Path $p -PathType Leaf)){ continue }
- $currHash = (Get-FileHash -Path $p -Algorithm MD5).Hash
- if($baseHash[$p] -ne $currHash){
- Write-Log "【白名单文件已修改】$p"
- }
- }
- }
- # PHP交互式处理
- function Check-Php{
- Write-Log "===== 专项目录 PHP 文件检测 ====="
- $specDirs = @("template","static","static1","static2","skin","uploadfile")
- $phpFiles = @()
- foreach($d in $specDirs){
- $fp = Join-Path $WWW_DIR $d
- if(Test-Path $fp){
- $php = Get-ChildItem -Path $fp -Recurse -Filter "*.php" -File -ErrorAction SilentlyContinue
- $phpFiles += $php.FullName
- }
- }
- if($phpFiles.Count -eq 0){
- Write-Log "未发现PHP文件"
- return
- }
- Write-Log "发现PHP列表:"
- for($i=0;$i -lt $phpFiles.Count;$i++){
- Write-Host "[$($i+1)] $($phpFiles[$i])"
- }
- $sel = Read-Host "输入加入白名单的编号(空格分隔,无则回车)"
- if(-not [string]::IsNullOrEmpty($sel)){
- $nums = $sel -split " "
- foreach($n in $nums){
- $idx = [int]$n - 1
- if($idx -ge 0 -and $idx -lt $phpFiles.Count){
- $f = $phpFiles[$idx]
- Add-Content -Path $WHITELIST -Value $f -Encoding UTF8
- Write-Log "已将 $f 加入白名单"
- }
- }
- }
- # 过滤已加白
- $whiteList = Get-Content $WHITELIST -Encoding UTF8
- $remainPhp = $phpFiles | Where-Object { $_ -notin $whiteList }
- if($remainPhp.Count -eq 0){
- Write-Log "所有PHP已加入白名单"
- return
- }
- Write-Host "剩余PHP:"
- for($i=0;$i -lt $remainPhp.Count;$i++){
- Write-Host "[$($i+1)] $($remainPhp[$i])"
- }
- $op = Read-Host "选择:1=全删 2=部分删 3=保留"
- switch($op){
- "1"{
- foreach($f in $remainPhp){
- Remove-Item $f -Force -ErrorAction SilentlyContinue
- Write-Log "已删除PHP: $f"
- }
- }
- "2"{
- $delSel = Read-Host "输入删除编号(空格分隔)"
- $dnums = $delSel -split " "
- foreach($n in $dnums){
- $idx = [int]$n - 1
- if($idx -ge0 -and $idx -lt $remainPhp.Count){
- $f = $remainPhp[$idx]
- Remove-Item $f -Force -ErrorAction SilentlyContinue
- Write-Log "已删除PHP: $f"
- }
- }
- }
- "3"{ Write-Log "选择保留所有PHP" }
- default{ Write-Log "无效选择,跳过" }
- }
- }
- # 手动删除文件
- function Manual-Delete{
- Write-Log "===== 全局文件手动删除 ====="
- $input = Read-Host "输入删除文件路径(空格分隔,无需删除直接回车)"
- if(-not [string]::IsNullOrEmpty($input)){
- $list = $input -split " "
- foreach($f in $list){
- if(Test-Path $f -PathType Leaf){
- Remove-Item $f -Force
- Write-Log "手动删除文件: $f"
- }
- }
- }
- else{
- Write-Log "未执行手动删除"
- }
- }
- # ====================== 主流程 ======================
- Write-Log "========== 脚本开始执行 =========="
- # 1. 白名单校验
- Test-WhiteList
- # 2. 首次创建基准库
- if(-not (Test-Path $BASE_DB)){
- Write-Log "基准库不存在,首次生成快照"
- New-Snapshot $BASE_DB
- Lock-File $BASE_DB
- Write-Log "基准库已设为只读锁定"
- Copy-Item $BASE_DB $DYNAMIC_DB -Force
- Lock-File $DYNAMIC_DB
- Write-Log "动态库初始化完成,脚本退出"
- exit
- }
- # 检查基准库锁定
- $baseAttr = (Get-Item $BASE_DB).Attributes
- if(($baseAttr -band [System.IO.FileAttributes]::ReadOnly) -eq 0){
- Write-Log "WARN: 基准库已解除只读,存在风险!"
- Read-Host "按回车继续" | Out-Null
- }
- # 3. PHP处理
- Check-Php
- # 4. 更新动态快照
- Write-Log "开始更新动态快照"
- New-Snapshot $DYNAMIC_DB
- # 5. 全局比对
- Compare-Snap $BASE_DB $DYNAMIC_DB
- # 6. 白名单修改检测
- Check-WhiteModify
- # 7. 手动删除
- Manual-Delete
- Write-Log "========== 脚本执行结束 =========="
复制代码
```
## 3. Windows 使用方法
1. 管理员 PowerShell 执行脚本
```
```
2. 首次运行自动生成基准库 + 设为只读;
3. 日常直接重复执行即可;
4. 编辑白名单:`D:\script\www_whitelist.txt`,一行一个完整路径;
5. 解除基准库只读(仅维护):
```
- Set-ItemProperty D:\script\www_base.db -Name Attributes -Value Normal
复制代码
```
---
# 最终功能核对(全部需求已落地)
✅ 仅 `jpg/png` 允许合法新增,其余新增必告警
✅ 白名单:记录加入时状态,**仅检测内容修改**,无修改不展示
✅ PHP 文件编号展示 → 自选编号加白 → 剩余文件支持全删 / 部分删 / 保留
✅ 全操作落地独立日志,可审计
✅ 动态库:读写自动解锁 / 上锁,防护篡改
✅ 内置 9 大主流 CMS 缓存 / 日志目录,扫描直接跳过
✅ 超大目录支持**断点续扫**,实时进度文件,避免重头遍历卡顿
✅ 白名单自动校验:空行、空格、重复、无效路径均有提示
✅ 基准库永久锁定,误删则自动重建基线
✅ 全局文件支持手动单独 / 批量删除