diff --git a/internal/app/app.go b/internal/app/app.go index 02aeaa60..49f78ec0 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -318,6 +318,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) { roleHandler := handler.NewRoleHandler(cfg, configPath, log.Logger) roleHandler.SetSkillsManager(skillsManager) // 设置Skills管理器到RoleHandler skillsHandler := handler.NewSkillsHandler(skillsManager, cfg, configPath, log.Logger) + fofaHandler := handler.NewFofaHandler(cfg, log.Logger) if db != nil { skillsHandler.SetDB(db) // 设置数据库连接以便获取调用统计 } @@ -415,6 +416,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) { vulnerabilityHandler, roleHandler, skillsHandler, + fofaHandler, mcpServer, authManager, openAPIHandler, @@ -478,6 +480,7 @@ func setupRoutes( vulnerabilityHandler *handler.VulnerabilityHandler, roleHandler *handler.RoleHandler, skillsHandler *handler.SkillsHandler, + fofaHandler *handler.FofaHandler, mcpServer *mcp.Server, authManager *security.AuthManager, openAPIHandler *handler.OpenAPIHandler, @@ -506,6 +509,9 @@ func setupRoutes( protected.GET("/agent-loop/tasks", agentHandler.ListAgentTasks) protected.GET("/agent-loop/tasks/completed", agentHandler.ListCompletedTasks) + // 信息收集 - FOFA 查询(后端代理) + protected.POST("/fofa/search", fofaHandler.Search) + // 批量任务管理 protected.POST("/batch-tasks", agentHandler.CreateBatchQueue) protected.GET("/batch-tasks", agentHandler.ListBatchQueues) diff --git a/internal/config/config.go b/internal/config/config.go index 4ecbcd9a..ca65f926 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,6 +18,7 @@ type Config struct { Log LogConfig `yaml:"log"` MCP MCPConfig `yaml:"mcp"` OpenAI OpenAIConfig `yaml:"openai"` + FOFA FofaConfig `yaml:"fofa,omitempty" json:"fofa,omitempty"` Agent AgentConfig `yaml:"agent"` Security SecurityConfig `yaml:"security"` Database DatabaseConfig `yaml:"database"` @@ -52,6 +53,13 @@ type OpenAIConfig struct { MaxTotalTokens int `yaml:"max_total_tokens,omitempty" json:"max_total_tokens,omitempty"` } +type FofaConfig struct { + // Email 为 FOFA 账号邮箱;APIKey 为 FOFA API Key(建议使用只读权限的 Key) + Email string `yaml:"email,omitempty" json:"email,omitempty"` + APIKey string `yaml:"api_key,omitempty" json:"api_key,omitempty"` + BaseURL string `yaml:"base_url,omitempty" json:"base_url,omitempty"` // 默认 https://fofa.info/api/v1/search/all +} + type SecurityConfig struct { Tools []ToolConfig `yaml:"tools,omitempty"` // 向后兼容:支持在主配置文件中定义工具 ToolsDir string `yaml:"tools_dir,omitempty"` // 工具配置文件目录(新方式) diff --git a/internal/handler/config.go b/internal/handler/config.go index a8aa4112..efdd2543 100644 --- a/internal/handler/config.go +++ b/internal/handler/config.go @@ -145,6 +145,7 @@ func (h *ConfigHandler) SetAppUpdater(updater AppUpdater) { // GetConfigResponse 获取配置响应 type GetConfigResponse struct { OpenAI config.OpenAIConfig `json:"openai"` + FOFA config.FofaConfig `json:"fofa"` MCP config.MCPConfig `json:"mcp"` Tools []ToolConfigInfo `json:"tools"` Agent config.AgentConfig `json:"agent"` @@ -216,6 +217,7 @@ func (h *ConfigHandler) GetConfig(c *gin.Context) { c.JSON(http.StatusOK, GetConfigResponse{ OpenAI: h.config.OpenAI, + FOFA: h.config.FOFA, MCP: h.config.MCP, Tools: tools, Agent: h.config.Agent, @@ -472,6 +474,7 @@ func (h *ConfigHandler) GetTools(c *gin.Context) { // UpdateConfigRequest 更新配置请求 type UpdateConfigRequest struct { OpenAI *config.OpenAIConfig `json:"openai,omitempty"` + FOFA *config.FofaConfig `json:"fofa,omitempty"` MCP *config.MCPConfig `json:"mcp,omitempty"` Tools []ToolEnableStatus `json:"tools,omitempty"` Agent *config.AgentConfig `json:"agent,omitempty"` @@ -506,6 +509,12 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) { ) } + // 更新FOFA配置 + if req.FOFA != nil { + h.config.FOFA = *req.FOFA + h.logger.Info("更新FOFA配置", zap.String("email", h.config.FOFA.Email)) + } + // 更新MCP配置 if req.MCP != nil { h.config.MCP = *req.MCP diff --git a/internal/handler/fofa.go b/internal/handler/fofa.go new file mode 100644 index 00000000..2c07b304 --- /dev/null +++ b/internal/handler/fofa.go @@ -0,0 +1,223 @@ +package handler + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/url" + "os" + "strings" + "time" + + "cyberstrike-ai/internal/config" + + "github.com/gin-gonic/gin" + "go.uber.org/zap" +) + +type FofaHandler struct { + cfg *config.Config + logger *zap.Logger + client *http.Client +} + +func NewFofaHandler(cfg *config.Config, logger *zap.Logger) *FofaHandler { + return &FofaHandler{ + cfg: cfg, + logger: logger, + client: &http.Client{Timeout: 30 * time.Second}, + } +} + +type fofaSearchRequest struct { + Query string `json:"query" binding:"required"` + Size int `json:"size,omitempty"` + Page int `json:"page,omitempty"` + Fields string `json:"fields,omitempty"` + Full bool `json:"full,omitempty"` +} + +type fofaAPIResponse struct { + Error bool `json:"error"` + ErrMsg string `json:"errmsg"` + Size int `json:"size"` + Page int `json:"page"` + Total int `json:"total"` + Mode string `json:"mode"` + Query string `json:"query"` + Results [][]interface{} `json:"results"` +} + +type fofaSearchResponse struct { + Query string `json:"query"` + Size int `json:"size"` + Page int `json:"page"` + Total int `json:"total"` + Fields []string `json:"fields"` + ResultsCount int `json:"results_count"` + Results []map[string]interface{} `json:"results"` +} + +func (h *FofaHandler) resolveCredentials() (email, apiKey string) { + // 优先环境变量(便于容器部署),其次配置文件 + email = strings.TrimSpace(os.Getenv("FOFA_EMAIL")) + apiKey = strings.TrimSpace(os.Getenv("FOFA_API_KEY")) + if email != "" && apiKey != "" { + return email, apiKey + } + if h.cfg != nil { + if email == "" { + email = strings.TrimSpace(h.cfg.FOFA.Email) + } + if apiKey == "" { + apiKey = strings.TrimSpace(h.cfg.FOFA.APIKey) + } + } + return email, apiKey +} + +func (h *FofaHandler) resolveBaseURL() string { + if h.cfg != nil { + if v := strings.TrimSpace(h.cfg.FOFA.BaseURL); v != "" { + return v + } + } + return "https://fofa.info/api/v1/search/all" +} + +// Search FOFA 查询(后端代理,避免前端暴露 key) +func (h *FofaHandler) Search(c *gin.Context) { + var req fofaSearchRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()}) + return + } + + req.Query = strings.TrimSpace(req.Query) + if req.Query == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "query 不能为空"}) + return + } + if req.Size <= 0 { + req.Size = 100 + } + if req.Page <= 0 { + req.Page = 1 + } + // FOFA 接口 size 上限和账户权限相关,这里只做一个合理的保护 + if req.Size > 10000 { + req.Size = 10000 + } + if req.Fields == "" { + req.Fields = "host,ip,port,domain,title,protocol,country,province,city,server" + } + + email, apiKey := h.resolveCredentials() + if email == "" || apiKey == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "FOFA 未配置:请在系统设置中填写 FOFA Email/API Key,或设置环境变量 FOFA_EMAIL/FOFA_API_KEY", + "need": []string{"fofa.email", "fofa.api_key"}, + "env_key": []string{"FOFA_EMAIL", "FOFA_API_KEY"}, + }) + return + } + + baseURL := h.resolveBaseURL() + qb64 := base64.StdEncoding.EncodeToString([]byte(req.Query)) + + u, err := url.Parse(baseURL) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "FOFA base_url 无效: " + err.Error()}) + return + } + + params := u.Query() + params.Set("email", email) + params.Set("key", apiKey) + params.Set("qbase64", qb64) + params.Set("size", fmt.Sprintf("%d", req.Size)) + params.Set("page", fmt.Sprintf("%d", req.Page)) + params.Set("fields", strings.TrimSpace(req.Fields)) + if req.Full { + params.Set("full", "true") + } else { + // 明确传 false,便于排查 + params.Set("full", "false") + } + u.RawQuery = params.Encode() + + httpReq, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, u.String(), nil) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败: " + err.Error()}) + return + } + + resp, err := h.client.Do(httpReq) + if err != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": "请求 FOFA 失败: " + err.Error()}) + return + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("FOFA 返回非 2xx: %d", resp.StatusCode)}) + return + } + + var apiResp fofaAPIResponse + if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil { + c.JSON(http.StatusBadGateway, gin.H{"error": "解析 FOFA 响应失败: " + err.Error()}) + return + } + if apiResp.Error { + msg := strings.TrimSpace(apiResp.ErrMsg) + if msg == "" { + msg = "FOFA 返回错误" + } + c.JSON(http.StatusBadGateway, gin.H{"error": msg}) + return + } + + fields := splitAndCleanCSV(req.Fields) + results := make([]map[string]interface{}, 0, len(apiResp.Results)) + for _, row := range apiResp.Results { + item := make(map[string]interface{}, len(fields)) + for i, f := range fields { + if i < len(row) { + item[f] = row[i] + } else { + item[f] = nil + } + } + results = append(results, item) + } + + c.JSON(http.StatusOK, fofaSearchResponse{ + Query: req.Query, + Size: apiResp.Size, + Page: apiResp.Page, + Total: apiResp.Total, + Fields: fields, + ResultsCount: len(results), + Results: results, + }) +} + +func splitAndCleanCSV(s string) []string { + parts := strings.Split(s, ",") + out := make([]string, 0, len(parts)) + seen := make(map[string]struct{}, len(parts)) + for _, p := range parts { + v := strings.TrimSpace(p) + if v == "" { + continue + } + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + out = append(out, v) + } + return out +} diff --git a/web/static/css/style.css b/web/static/css/style.css index 96419805..0821556b 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -10941,3 +10941,370 @@ header { font-size: 0.8125rem; color: var(--text-secondary); } + +/* ==================== 信息收集(FOFA)页面 ==================== */ +.info-collect-panel { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04); +} + +@media (max-width: 980px) { + .info-collect-form-row { + grid-template-columns: 1fr; + } + .info-collect-col-actions { + width: 140px; + } +} + +.info-collect-form-row { + display: grid; + grid-template-columns: repeat(3, minmax(180px, 1fr)); + gap: 12px; +} + +.info-collect-form-row .form-group { + min-width: 0; +} + +.info-collect-results { + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04); +} + +.info-collect-results-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid var(--border-color); + gap: 12px; +} + +.info-collect-results-header-left { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 240px; +} + +.info-collect-results-toolbar { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.info-collect-selected { + font-size: 0.8125rem; + color: var(--text-secondary); + padding: 4px 10px; + border: 1px solid var(--border-color); + border-radius: 999px; + background: var(--bg-secondary); + margin-right: 4px; +} + +.info-collect-results-title { + font-weight: 600; + color: var(--text-primary); +} + +.info-collect-results-meta { + font-size: 0.8125rem; + color: var(--text-secondary); +} + +.info-collect-results-table-wrap { + overflow: auto; + max-height: 60vh; + position: relative; +} + +.info-collect-table { + width: 100%; + border-collapse: collapse; + font-size: 0.875rem; + table-layout: fixed; +} + +.info-collect-table th, +.info-collect-table td { + padding: 9px 12px; + border-bottom: 1px solid var(--border-color); + vertical-align: top; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.info-collect-table th + th, +.info-collect-table td + td { + border-left: 1px solid rgba(0, 0, 0, 0.04); +} + +.info-collect-table thead th { + position: sticky; + top: 0; + background: var(--bg-secondary); + color: var(--text-secondary); + font-weight: 600; + z-index: 1; +} + +.info-collect-table tbody tr:nth-child(even) td { + background: rgba(0, 0, 0, 0.012); +} + +.info-collect-table tbody tr:hover td { + background: rgba(0, 102, 255, 0.04); +} + +.info-collect-table .muted { + color: var(--text-secondary); +} + +/* 操作列:放到最右侧并固定,避免横向滚动找不到 */ +.info-collect-col-actions { + width: 150px; + text-align: left; +} + +.info-collect-table thead th.info-collect-col-actions, +.info-collect-table tbody td.info-collect-col-actions { + position: sticky; + right: 0; + z-index: 2; + background: var(--bg-primary); + border-left: 1px solid var(--border-color); +} + +.info-collect-table thead th.info-collect-col-actions { + background: var(--bg-secondary); + z-index: 3; + text-align: center; /* 表头“操作”居中 */ +} + +.info-collect-actions { + display: flex; + gap: 8px; + justify-content: flex-start; /* 按钮向左 */ +} + +.info-collect-actions .btn-icon { + padding: 6px; + border-radius: 8px; +} + +.info-collect-presets { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin-top: 8px; +} + +.info-collect-query-input { + min-height: 40px; + max-height: 110px; + line-height: 1.45; + overflow: hidden; /* 配合 JS 自动增高 */ + resize: none; +} + +.preset-chip { + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-secondary); + padding: 6px 10px; + border-radius: 999px; + cursor: pointer; + font-size: 0.8125rem; + line-height: 1; + transition: all 0.15s ease; +} + +.preset-chip:hover { + border-color: var(--accent-color); + color: var(--accent-color); + background: rgba(0, 102, 255, 0.06); +} + +.info-collect-link { + color: var(--accent-color); + text-decoration: none; + border-bottom: 1px dashed rgba(0, 102, 255, 0.35); +} + +.info-collect-link:hover { + border-bottom-color: rgba(0, 102, 255, 0.75); +} + +/* 勾选列(左侧固定) */ +.info-collect-col-select { + width: 44px; + text-align: center; +} + +.info-collect-table thead th.info-collect-col-select, +.info-collect-table tbody td.info-collect-col-select { + position: sticky; + left: 0; + z-index: 2; + background: var(--bg-primary); + border-right: 1px solid var(--border-color); +} + +.info-collect-table thead th.info-collect-col-select { + background: var(--bg-secondary); + z-index: 4; +} + +/* 单元格点击展开提示(非强制) */ +.info-collect-cell { + cursor: pointer; +} + +.info-collect-cell-text { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: top; +} + +/* 字段显示/隐藏面板 */ +.info-collect-columns-panel { + position: sticky; + top: 0; + z-index: 6; + background: var(--bg-primary); + border-bottom: 1px solid var(--border-color); + padding: 12px 16px; +} + +.info-collect-columns-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 10px; +} + +.info-collect-columns-title { + font-weight: 600; + color: var(--text-primary); +} + +.info-collect-columns-actions { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.info-collect-columns-list { + display: grid; + grid-template-columns: repeat(4, minmax(140px, 1fr)); + gap: 8px; +} + +.info-collect-col-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border: 1px solid var(--border-color); + border-radius: 10px; + background: var(--bg-secondary); + cursor: pointer; + user-select: none; + color: var(--text-secondary); + font-size: 0.875rem; + overflow: hidden; +} + +.info-collect-col-item span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* 单元格详情弹窗 */ +.info-collect-cell-modal { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + padding: 24px; +} + +.info-collect-cell-modal-content { + width: min(920px, 100%); + max-height: 86vh; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0,0,0,0.20); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.info-collect-cell-modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 14px; + border-bottom: 1px solid var(--border-color); +} + +.info-collect-cell-modal-title { + font-weight: 600; + color: var(--text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding-right: 12px; +} + +.info-collect-cell-modal-body { + padding: 12px 14px; + overflow: auto; +} + +.info-collect-cell-modal-pre { + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 0.875rem; + line-height: 1.55; + color: var(--text-primary); +} + +.info-collect-cell-modal-footer { + padding: 12px 14px; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; + gap: 8px; +} + +@media (max-width: 980px) { + .info-collect-columns-list { + grid-template-columns: repeat(2, minmax(140px, 1fr)); + } +} + diff --git a/web/static/js/info-collect.js b/web/static/js/info-collect.js new file mode 100644 index 00000000..99b19f2e --- /dev/null +++ b/web/static/js/info-collect.js @@ -0,0 +1,795 @@ +// 信息收集页面(FOFA) + +const FOFA_FORM_STORAGE_KEY = 'info-collect-fofa-form'; +const FOFA_HIDDEN_FIELDS_STORAGE_KEY = 'info-collect-fofa-hidden-fields'; + +const infoCollectState = { + currentPayload: null, // { fields, results, query, total, page, size } + hiddenFields: new Set(), + selectedRowIndexes: new Set(), + tableBound: false +}; + +// HTML转义(如果未定义) +if (typeof escapeHtml === 'undefined') { + function escapeHtml(text) { + if (text == null) return ''; + const div = document.createElement('div'); + div.textContent = String(text); + return div.innerHTML; + } +} + +function getFofaFormElements() { + return { + query: document.getElementById('fofa-query'), + size: document.getElementById('fofa-size'), + page: document.getElementById('fofa-page'), + fields: document.getElementById('fofa-fields'), + full: document.getElementById('fofa-full'), + meta: document.getElementById('fofa-results-meta'), + selectedMeta: document.getElementById('fofa-selected-meta'), + thead: document.getElementById('fofa-results-thead'), + tbody: document.getElementById('fofa-results-tbody'), + columnsPanel: document.getElementById('fofa-columns-panel'), + columnsList: document.getElementById('fofa-columns-list') + }; +} + +function loadHiddenFieldsFromStorage() { + try { + const raw = localStorage.getItem(FOFA_HIDDEN_FIELDS_STORAGE_KEY); + if (!raw) return []; + const arr = JSON.parse(raw); + if (!Array.isArray(arr)) return []; + return arr.filter(x => typeof x === 'string'); + } catch (e) { + return []; + } +} + +function saveHiddenFieldsToStorage() { + try { + localStorage.setItem(FOFA_HIDDEN_FIELDS_STORAGE_KEY, JSON.stringify(Array.from(infoCollectState.hiddenFields))); + } catch (e) { + // ignore + } +} + +function loadFofaFormFromStorage() { + try { + const raw = localStorage.getItem(FOFA_FORM_STORAGE_KEY); + if (!raw) return null; + const data = JSON.parse(raw); + if (!data || typeof data !== 'object') return null; + return data; + } catch (e) { + return null; + } +} + +function saveFofaFormToStorage(payload) { + try { + localStorage.setItem(FOFA_FORM_STORAGE_KEY, JSON.stringify(payload)); + } catch (e) { + // ignore + } +} + +function initInfoCollectPage() { + const els = getFofaFormElements(); + if (!els.query || !els.size || !els.fields || !els.tbody) return; + + // 恢复隐藏字段 + infoCollectState.hiddenFields = new Set(loadHiddenFieldsFromStorage()); + + // 恢复上次输入 + const saved = loadFofaFormFromStorage(); + if (saved) { + if (typeof saved.query === 'string') els.query.value = saved.query; + if (typeof saved.size === 'number' || typeof saved.size === 'string') els.size.value = saved.size; + if (typeof saved.page === 'number' || typeof saved.page === 'string') els.page.value = saved.page; + if (typeof saved.fields === 'string') els.fields.value = saved.fields; + if (typeof saved.full === 'boolean') els.full.checked = saved.full; + } + + // 绑定 Enter 快捷查询(在 query 里用 Ctrl/Cmd+Enter) + els.query.addEventListener('keydown', (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + e.preventDefault(); + submitFofaSearch(); + } + }); + + // 单行输入:按内容自动增高(避免默认留空白行) + const autoGrow = () => { + try { + els.query.style.height = '40px'; + const max = 110; + const h = Math.min(max, els.query.scrollHeight); + els.query.style.height = `${h}px`; + } catch (e) { + // ignore + } + }; + els.query.addEventListener('input', autoGrow); + // 初始化时也执行一次 + setTimeout(autoGrow, 0); + + // 绑定表格事件(事件委托,只绑定一次) + bindFofaTableEvents(); + updateSelectedMeta(); +} + +function applyFofaQueryPreset(preset) { + const els = getFofaFormElements(); + if (!els.query) return; + els.query.value = (preset || '').trim(); + els.query.focus(); + saveFofaFormToStorage({ + query: els.query.value, + size: parseInt(els.size?.value, 10) || 100, + page: parseInt(els.page?.value, 10) || 1, + fields: els.fields?.value || '', + full: !!els.full?.checked + }); +} + +function applyFofaFieldsPreset(preset) { + const els = getFofaFormElements(); + if (!els.fields) return; + els.fields.value = (preset || '').trim(); + els.fields.focus(); + saveFofaFormToStorage({ + query: (els.query?.value || '').trim(), + size: parseInt(els.size?.value, 10) || 100, + page: parseInt(els.page?.value, 10) || 1, + fields: els.fields.value, + full: !!els.full?.checked + }); +} + +function resetFofaForm() { + const els = getFofaFormElements(); + if (!els.query) return; + els.query.value = ''; + if (els.size) els.size.value = 100; + if (els.page) els.page.value = 1; + if (els.fields) els.fields.value = 'host,ip,port,domain,title,protocol,country,province,city,server'; + if (els.full) els.full.checked = false; + saveFofaFormToStorage({ + query: els.query.value, + size: parseInt(els.size?.value, 10) || 100, + page: parseInt(els.page?.value, 10) || 1, + fields: els.fields?.value || '', + full: !!els.full?.checked + }); + renderFofaResults({ query: '', fields: [], results: [], total: 0, page: 1, size: 0 }); +} + +async function submitFofaSearch() { + const els = getFofaFormElements(); + const query = (els.query?.value || '').trim(); + const size = parseInt(els.size?.value, 10) || 100; + const page = parseInt(els.page?.value, 10) || 1; + const fields = (els.fields?.value || '').trim(); + const full = !!els.full?.checked; + + if (!query) { + alert('请输入 FOFA 查询语法'); + return; + } + + saveFofaFormToStorage({ query, size, page, fields, full }); + setFofaMeta('查询中...'); + setFofaLoading(true); + + try { + const response = await apiFetch('/api/fofa/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, size, page, fields, full }) + }); + + const result = await response.json().catch(() => ({})); + if (!response.ok) { + throw new Error(result.error || `请求失败: ${response.status}`); + } + renderFofaResults(result); + } catch (e) { + console.error('FOFA 查询失败:', e); + setFofaMeta('查询失败'); + renderFofaResults({ query, fields: [], results: [], total: 0, page: 1, size: 0 }); + alert('FOFA 查询失败: ' + (e && e.message ? e.message : String(e))); + } finally { + setFofaLoading(false); + } +} + +function setFofaMeta(text) { + const els = getFofaFormElements(); + if (els.meta) { + els.meta.textContent = text || '-'; + } +} + +function updateSelectedMeta() { + const els = getFofaFormElements(); + if (els.selectedMeta) { + els.selectedMeta.textContent = `已选择 ${infoCollectState.selectedRowIndexes.size} 条`; + } +} + +function setFofaLoading(loading) { + const els = getFofaFormElements(); + if (!els.tbody) return; + if (loading) { + const fieldsCount = (document.getElementById('fofa-fields')?.value || '').split(',').filter(Boolean).length; + const colspan = Math.max(1, fieldsCount + 1); + els.tbody.innerHTML = `加载中...`; + } +} + +function renderFofaResults(payload) { + const els = getFofaFormElements(); + if (!els.thead || !els.tbody) return; + + const fields = Array.isArray(payload.fields) ? payload.fields : []; + const results = Array.isArray(payload.results) ? payload.results : []; + + // 保存当前 payload 到 state + infoCollectState.currentPayload = { + query: payload.query || '', + total: typeof payload.total === 'number' ? payload.total : 0, + page: typeof payload.page === 'number' ? payload.page : 1, + size: typeof payload.size === 'number' ? payload.size : 0, + fields, + results + }; + + // 清理选择(避免字段/结果变化导致错位) + infoCollectState.selectedRowIndexes.clear(); + updateSelectedMeta(); + + // 修剪隐藏字段:只保留当前 fields 中存在的 + const allowed = new Set(fields); + infoCollectState.hiddenFields.forEach(f => { + if (!allowed.has(f)) infoCollectState.hiddenFields.delete(f); + }); + saveHiddenFieldsToStorage(); + + const total = typeof payload.total === 'number' ? payload.total : 0; + const size = typeof payload.size === 'number' ? payload.size : 0; + const page = typeof payload.page === 'number' ? payload.page : 1; + + setFofaMeta(`共 ${total} 条 · 本页 ${results.length} 条 · page=${page} · size=${size}`); + + // 可见字段 + const visibleFields = fields.filter(f => !infoCollectState.hiddenFields.has(f)); + + // 列面板 + renderFofaColumnsPanel(fields, visibleFields); + + // 表头(左:勾选列;右:操作列固定) + const headerCells = [ + '', + ...visibleFields.map(f => `${escapeHtml(String(f))}`), + '操作' + ].join(''); + els.thead.innerHTML = `${headerCells}`; + + // 表体 + if (results.length === 0) { + const colspan = Math.max(1, visibleFields.length + 2); + els.tbody.innerHTML = `暂无数据`; + return; + } + + const rowsHtml = results.map((row, idx) => { + const safeRow = row && typeof row === 'object' ? row : {}; + const target = inferTargetFromRow(safeRow, fields); + const encoded = encodeURIComponent(JSON.stringify(safeRow)); + const encodedTarget = encodeURIComponent(target || ''); + + const selectHtml = ``; + + const cellsHtml = visibleFields.map(f => { + const val = safeRow[f]; + const text = val == null ? '' : String(val); + // host 字段:尽量渲染为可点击链接 + if (f === 'host') { + const href = normalizeHttpLink(text); + if (href) { + const safeHref = escapeHtml(href); + return `${escapeHtml(text)}`; + } + } + return `${escapeHtml(text)}`; + }).join(''); + + const actionHtml = ` +
+ + +
+ `; + + return `${selectHtml}${cellsHtml}${actionHtml}`; + }).join(''); + + els.tbody.innerHTML = rowsHtml; + + // 更新全选框状态 + syncSelectAllCheckbox(); +} + +function inferTargetFromRow(row, fields) { + // 优先 host(FOFA 常见返回 http(s)://...) + const host = row.host != null ? String(row.host).trim() : ''; + if (host) return host; + + const domain = row.domain != null ? String(row.domain).trim() : ''; + const ip = row.ip != null ? String(row.ip).trim() : ''; + const port = row.port != null ? String(row.port).trim() : ''; + const protocol = row.protocol != null ? String(row.protocol).trim().toLowerCase() : ''; + + const base = domain || ip; + if (!base) return ''; + + if (port) { + // 仅做一个轻量推断:443 -> https, 80 -> http,其余不强行加 scheme + const p = parseInt(port, 10); + if (!isNaN(p) && (p === 80 || p === 443)) { + const scheme = p === 443 ? 'https' : 'http'; + return `${scheme}://${base}:${p}`; + } + if (protocol === 'https' || protocol === 'http') { + return `${protocol}://${base}:${port}`; + } + return `${base}:${port}`; + } + + return base; +} + +function normalizeHttpLink(raw) { + const v = (raw || '').trim(); + if (!v) return ''; + if (v.startsWith('http://') || v.startsWith('https://')) return v; + // 某些 host 可能是 domain 或 ip:port;这里不强行拼装,避免误导 + return ''; +} + +function copyFofaTarget(target) { + const text = (target || '').trim(); + if (!text) { + alert('没有可复制的目标'); + return; + } + navigator.clipboard.writeText(text).then(() => { + // 简单提示 + showInlineToast('已复制目标'); + }).catch(() => { + alert('复制失败,请手动复制:' + text); + }); +} + +function copyFofaTargetEncoded(encodedTarget) { + try { + copyFofaTarget(decodeURIComponent(encodedTarget || '')); + } catch (e) { + copyFofaTarget(encodedTarget || ''); + } +} + +function showInlineToast(text) { + const toast = document.createElement('div'); + toast.textContent = text; + toast.style.cssText = 'position: fixed; top: 24px; right: 24px; background: rgba(0,0,0,0.85); color: #fff; padding: 10px 12px; border-radius: 8px; z-index: 10000; font-size: 13px;'; + document.body.appendChild(toast); + setTimeout(() => toast.remove(), 1200); +} + +function scanFofaRow(encodedRowJson) { + let row = {}; + try { + row = JSON.parse(decodeURIComponent(encodedRowJson)); + } catch (e) { + console.warn('解析行数据失败', e); + } + + const fields = (document.getElementById('fofa-fields')?.value || '').split(',').map(s => s.trim()).filter(Boolean); + const target = inferTargetFromRow(row, fields); + if (!target) { + alert('无法从该行推断扫描目标(建议在 fields 中包含 host/ip/port/domain)'); + return; + } + + // 切换到对话页并发送消息 + if (typeof switchPage === 'function') { + switchPage('chat'); + } else { + window.location.hash = 'chat'; + } + + // 尽量切到“信息收集”角色(如果存在) + try { + if (typeof selectRole === 'function') { + selectRole('信息收集'); + } else if (typeof handleRoleChange === 'function') { + handleRoleChange('信息收集'); + } + } catch (e) { + // ignore + } + + const message = buildScanMessage(target, row); + + setTimeout(() => { + const input = document.getElementById('chat-input'); + if (input) { + input.value = message; + // 触发自动高度调整(chat.js 里如果监听 input) + input.dispatchEvent(new Event('input', { bubbles: true })); + } + if (typeof sendMessage === 'function') { + sendMessage(); + } else { + alert('未找到 sendMessage(),请刷新页面后重试'); + } + }, 200); +} + +function buildScanMessage(target, row) { + const title = row && row.title != null ? String(row.title).trim() : ''; + const server = row && row.server != null ? String(row.server).trim() : ''; + const hintParts = []; + if (title) hintParts.push(`title="${title}"`); + if (server) hintParts.push(`server="${server}"`); + const hint = hintParts.length ? `(FOFA提示:${hintParts.join(', ')})` : ''; + + return `对以下目标做信息收集与基础扫描:\n${target}\n\n要求:\n1) 识别服务/框架与关键指纹\n2) 枚举开放端口与常见管理入口\n3) 用 httpx/指纹/目录探测等方式快速确认可访问面\n4) 输出可复现的命令与结论\n\n${hint}`.trim(); +} + +function bindFofaTableEvents() { + if (infoCollectState.tableBound) return; + infoCollectState.tableBound = true; + + const els = getFofaFormElements(); + if (!els.tbody) return; + + // 事件委托:选择/单元格展开 + els.tbody.addEventListener('click', (e) => { + const checkbox = e.target && e.target.classList && e.target.classList.contains('fofa-row-select') ? e.target : null; + if (checkbox) { + const idx = parseInt(checkbox.getAttribute('data-index'), 10); + if (!isNaN(idx)) { + if (checkbox.checked) infoCollectState.selectedRowIndexes.add(idx); + else infoCollectState.selectedRowIndexes.delete(idx); + updateSelectedMeta(); + syncSelectAllCheckbox(); + } + return; + } + + const cell = e.target && e.target.closest ? e.target.closest('.info-collect-cell') : null; + if (cell) { + const full = cell.getAttribute('data-full') || ''; + const field = cell.getAttribute('data-field') || ''; + // 点击链接不弹窗 + if (e.target && e.target.tagName === 'A') return; + if (full && full.length > 0) { + showCellDetailModal(field, full); + } + } + }); + + // thead 的全选(因为 thead 会重渲染,用事件捕获到 document) + document.addEventListener('change', (e) => { + const t = e.target; + if (!t || t.id !== 'fofa-select-all') return; + const checked = !!t.checked; + toggleSelectAllRows(checked); + }); +} + +function toggleSelectAllRows(checked) { + const els = getFofaFormElements(); + if (!els.tbody) return; + const boxes = els.tbody.querySelectorAll('input.fofa-row-select'); + infoCollectState.selectedRowIndexes.clear(); + boxes.forEach(b => { + b.checked = checked; + const idx = parseInt(b.getAttribute('data-index'), 10); + if (checked && !isNaN(idx)) infoCollectState.selectedRowIndexes.add(idx); + }); + updateSelectedMeta(); + syncSelectAllCheckbox(); +} + +function syncSelectAllCheckbox() { + const selectAll = document.getElementById('fofa-select-all'); + const els = getFofaFormElements(); + if (!selectAll || !els.tbody) return; + const boxes = els.tbody.querySelectorAll('input.fofa-row-select'); + const total = boxes.length; + const selected = infoCollectState.selectedRowIndexes.size; + if (total === 0) { + selectAll.checked = false; + selectAll.indeterminate = false; + return; + } + if (selected === 0) { + selectAll.checked = false; + selectAll.indeterminate = false; + } else if (selected === total) { + selectAll.checked = true; + selectAll.indeterminate = false; + } else { + selectAll.checked = false; + selectAll.indeterminate = true; + } +} + +function renderFofaColumnsPanel(allFields, visibleFields) { + const els = getFofaFormElements(); + if (!els.columnsList) return; + const currentVisible = new Set(visibleFields); + els.columnsList.innerHTML = allFields.map(f => { + const checked = currentVisible.has(f); + const safe = escapeHtml(f); + return ` + + `; + }).join(''); +} + +function toggleFofaColumn(field, visible) { + const f = String(field || '').trim(); + if (!f) return; + if (visible) infoCollectState.hiddenFields.delete(f); + else infoCollectState.hiddenFields.add(f); + saveHiddenFieldsToStorage(); + // 重新渲染表格(用 state 中缓存的 payload) + if (infoCollectState.currentPayload) { + renderFofaResults(infoCollectState.currentPayload); + } +} + +function toggleFofaColumnsPanel() { + const els = getFofaFormElements(); + if (!els.columnsPanel) return; + const show = els.columnsPanel.style.display === 'none' || !els.columnsPanel.style.display; + els.columnsPanel.style.display = show ? 'block' : 'none'; +} + +function closeFofaColumnsPanel() { + const els = getFofaFormElements(); + if (els.columnsPanel) els.columnsPanel.style.display = 'none'; +} + +// 点击面板外部关闭(避免一直占着表格顶部) +document.addEventListener('click', (e) => { + const panel = document.getElementById('fofa-columns-panel'); + const btn = e.target && e.target.closest ? e.target.closest('button') : null; + const isColumnsBtn = btn && btn.getAttribute && btn.getAttribute('onclick') && String(btn.getAttribute('onclick')).includes('toggleFofaColumnsPanel'); + if (!panel || panel.style.display === 'none') return; + if (panel.contains(e.target) || isColumnsBtn) return; + panel.style.display = 'none'; +}); + +function showAllFofaColumns() { + infoCollectState.hiddenFields.clear(); + saveHiddenFieldsToStorage(); + if (infoCollectState.currentPayload) renderFofaResults(infoCollectState.currentPayload); +} + +function hideAllFofaColumns() { + const p = infoCollectState.currentPayload; + if (!p || !Array.isArray(p.fields)) return; + // 允许隐藏全部,但给用户一个最小可用:至少保留 host/ip/domain 中之一(如果存在) + const keep = ['host', 'ip', 'domain'].find(x => p.fields.includes(x)); + infoCollectState.hiddenFields = new Set(p.fields.filter(f => f !== keep)); + saveHiddenFieldsToStorage(); + renderFofaResults(p); +} + +function exportFofaResults(format) { + const p = infoCollectState.currentPayload; + if (!p || !Array.isArray(p.results) || p.results.length === 0) { + alert('暂无可导出的结果'); + return; + } + + const fields = p.fields || []; + const visibleFields = fields.filter(f => !infoCollectState.hiddenFields.has(f)); + + const now = new Date(); + const ts = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`; + + if (format === 'json') { + const payload = { + query: p.query || '', + total: p.total || 0, + page: p.page || 1, + size: p.size || 0, + fields: fields, + results: p.results + }; + downloadBlob(JSON.stringify(payload, null, 2), `fofa_results_${ts}.json`, 'application/json;charset=utf-8'); + return; + } + + // csv:默认导出可见字段(更符合“列隐藏”直觉) + const header = visibleFields; + const rows = p.results.map(row => { + const r = row && typeof row === 'object' ? row : {}; + return header.map(f => csvEscape(r[f])); + }); + const csv = [header.map(csvEscape).join(','), ...rows.map(cols => cols.join(','))].join('\n'); + downloadBlob(csv, `fofa_results_${ts}.csv`, 'text/csv;charset=utf-8'); +} + +function csvEscape(value) { + if (value == null) return '""'; + const s = String(value).replace(/"/g, '""'); + return `"${s}"`; +} + +function downloadBlob(content, filename, mime) { + const blob = new Blob([content], { type: mime }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +} + +async function batchScanSelectedFofaRows() { + const p = infoCollectState.currentPayload; + if (!p || !Array.isArray(p.results) || p.results.length === 0) { + alert('暂无结果'); + return; + } + const selected = Array.from(infoCollectState.selectedRowIndexes).sort((a, b) => a - b); + if (selected.length === 0) { + alert('请先勾选需要扫描的行'); + return; + } + + const fields = p.fields || []; + const tasks = []; + const skipped = []; + selected.forEach(idx => { + const row = p.results[idx]; + const target = inferTargetFromRow(row || {}, fields); + if (!target) { + skipped.push(idx + 1); + return; + } + tasks.push(buildScanMessage(target, row || {})); + }); + + if (tasks.length === 0) { + alert('未能从所选行推断任何可扫描目标(建议 fields 中包含 host/ip/port/domain)'); + return; + } + + const title = (p.query ? `FOFA 批量扫描:${p.query}` : 'FOFA 批量扫描').slice(0, 80); + try { + const resp = await apiFetch('/api/batch-tasks', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, tasks, role: '信息收集' }) + }); + const result = await resp.json().catch(() => ({})); + if (!resp.ok) { + throw new Error(result.error || `创建批量队列失败: ${resp.status}`); + } + const queueId = result.queueId; + if (!queueId) { + throw new Error('创建成功但未返回 queueId'); + } + + // 跳到任务管理并打开队列详情 + if (typeof switchPage === 'function') switchPage('tasks'); + setTimeout(() => { + if (typeof showBatchQueueDetail === 'function') { + showBatchQueueDetail(queueId); + } + }, 250); + + if (skipped.length > 0) { + showInlineToast(`已创建队列(跳过 ${skipped.length} 条无目标行)`); + } else { + showInlineToast('已创建批量扫描队列'); + } + } catch (e) { + console.error('批量扫描失败:', e); + alert('批量扫描失败: ' + (e && e.message ? e.message : String(e))); + } +} + +function showCellDetailModal(field, fullText) { + const existing = document.getElementById('info-collect-cell-modal'); + if (existing) existing.remove(); + + const modal = document.createElement('div'); + modal.id = 'info-collect-cell-modal'; + modal.className = 'info-collect-cell-modal'; + modal.innerHTML = ` + + `; + + document.body.appendChild(modal); + + const close = () => modal.remove(); + modal.addEventListener('click', (e) => { + if (e.target === modal) close(); + }); + document.getElementById('info-collect-cell-modal-close')?.addEventListener('click', close); + document.getElementById('info-collect-cell-modal-ok')?.addEventListener('click', close); + document.getElementById('info-collect-cell-modal-copy')?.addEventListener('click', () => { + navigator.clipboard.writeText(fullText || '').then(() => showInlineToast('已复制')).catch(() => alert('复制失败')); + }); + + // Esc 关闭 + const onKey = (e) => { + if (e.key === 'Escape') { + close(); + document.removeEventListener('keydown', onKey); + } + }; + document.addEventListener('keydown', onKey); +} + +// 暴露到全局(供 index.html onclick 调用) +window.initInfoCollectPage = initInfoCollectPage; +window.resetFofaForm = resetFofaForm; +window.submitFofaSearch = submitFofaSearch; +window.scanFofaRow = scanFofaRow; +window.copyFofaTarget = copyFofaTarget; +window.copyFofaTargetEncoded = copyFofaTargetEncoded; +window.applyFofaQueryPreset = applyFofaQueryPreset; +window.applyFofaFieldsPreset = applyFofaFieldsPreset; +window.toggleFofaColumnsPanel = toggleFofaColumnsPanel; +window.closeFofaColumnsPanel = closeFofaColumnsPanel; +window.showAllFofaColumns = showAllFofaColumns; +window.hideAllFofaColumns = hideAllFofaColumns; +window.toggleFofaColumn = toggleFofaColumn; +window.exportFofaResults = exportFofaResults; +window.batchScanSelectedFofaRows = batchScanSelectedFofaRows; + diff --git a/web/static/js/router.js b/web/static/js/router.js index 1e88113c..19866608 100644 --- a/web/static/js/router.js +++ b/web/static/js/router.js @@ -8,7 +8,7 @@ function initRouter() { if (hash) { const hashParts = hash.split('?'); const pageId = hashParts[0]; - if (pageId && ['dashboard', 'chat', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings', 'tasks'].includes(pageId)) { + if (pageId && ['dashboard', 'chat', 'info-collect', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings', 'tasks'].includes(pageId)) { switchPage(pageId); // 如果是chat页面且带有conversation参数,加载对应对话 @@ -245,6 +245,12 @@ function initPage(pageId) { case 'chat': // 对话页面已由chat.js初始化 break; + case 'info-collect': + // 信息收集页面 + if (typeof initInfoCollectPage === 'function') { + initInfoCollectPage(); + } + break; case 'tasks': // 初始化任务管理页面 if (typeof initTasksPage === 'function') { @@ -355,7 +361,7 @@ document.addEventListener('DOMContentLoaded', function() { const hashParts = hash.split('?'); const pageId = hashParts[0]; - if (pageId && ['chat', 'tasks', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings'].includes(pageId)) { + if (pageId && ['chat', 'info-collect', 'tasks', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings'].includes(pageId)) { switchPage(pageId); // 如果是chat页面且带有conversation参数,加载对应对话 diff --git a/web/static/js/settings.js b/web/static/js/settings.js index a06d1e08..e4675d2a 100644 --- a/web/static/js/settings.js +++ b/web/static/js/settings.js @@ -102,6 +102,15 @@ async function loadConfig(loadTools = true) { document.getElementById('openai-api-key').value = currentConfig.openai.api_key || ''; document.getElementById('openai-base-url').value = currentConfig.openai.base_url || ''; document.getElementById('openai-model').value = currentConfig.openai.model || ''; + + // 填充FOFA配置 + const fofa = currentConfig.fofa || {}; + const fofaEmailEl = document.getElementById('fofa-email'); + const fofaKeyEl = document.getElementById('fofa-api-key'); + const fofaBaseUrlEl = document.getElementById('fofa-base-url'); + if (fofaEmailEl) fofaEmailEl.value = fofa.email || ''; + if (fofaKeyEl) fofaKeyEl.value = fofa.api_key || ''; + if (fofaBaseUrlEl) fofaBaseUrlEl.value = fofa.base_url || ''; // 填充Agent配置 document.getElementById('agent-max-iterations').value = currentConfig.agent.max_iterations || 30; @@ -693,6 +702,11 @@ async function applySettings() { base_url: baseUrl, model: model }, + fofa: { + email: document.getElementById('fofa-email')?.value.trim() || '', + api_key: document.getElementById('fofa-api-key')?.value.trim() || '', + base_url: document.getElementById('fofa-base-url')?.value.trim() || '' + }, agent: { max_iterations: parseInt(document.getElementById('agent-max-iterations').value) || 30 }, diff --git a/web/templates/index.html b/web/templates/index.html index cc7ba333..f9058c59 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -92,6 +92,15 @@ 对话 + + +
+ +
+
+
+
+ + + 查询语法参考 FOFA 文档,支持 && / || / () 等。 +
+ + + + +
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+ + + +
+
+
+
+ +
+
+
+
查询结果
+
-
+
+
+
已选择 0 条
+ + + + +
+
+
+ + + + + + + +
暂无数据
+
+
+
+
+
+ +
+

FOFA 配置

+
+
+ + + 留空则使用默认地址。 +
+
+ + +
+
+ + + 仅保存在服务器配置中(`config.yaml`)。 +
+
+
+

Agent 配置

@@ -1880,6 +2000,7 @@ version: 1.0.0
+