mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-19 22:17:42 +02:00
fix: restore listener edit actions and validate required fields
This commit is contained in:
1 parent
ae395f5bf7
commit
7d6d3e77dc
10 files changed
+186
-17
No files matched your search
@@ -80,3 +80,9 @@
|
||||
- Excel 第 8、9 行:截图与模板对照证明灰色 CN/Beijing 是 placeholder,省市是手工输入,不是时区识别结果。示例增加中、英、俄翻译;不修改用户保存的地理字段,也不新增地区识别或联动能力。
|
||||
- Excel 第 10 行:CSV 与 XLSX 表头采用当前语言,环境、重要性、状态与已知风险等级显示本地化标签。原始地址、地区、项目、标签、未知枚举值保持原样。导入兼容旧英文列名与当前语言的导出列名、枚举值。
|
||||
- 验证:7 项测试通过,包含使用仓库内实际 SheetJS 生成和读取 CSV/XLSX,再调用真实导入解析;中、英、俄三种语言均验证地址、项目归属、带引号/换行标题、标签和枚举值不丢失。
|
||||
|
||||
## 追加:监听器表单与保存入口
|
||||
|
||||
- Excel 第 50 行:编辑保存使用 document 点击委托,但 c2-modal 内层 inline stopPropagation 阻止按钮到达委托。移除内层阻断;背景关闭本来已检查 event.target===this,不会因内部点击关闭。验证实际委托分发到真实 saveListener 后发送 PUT 并在成功后关闭。
|
||||
- Excel 第 23 行:名称、类型、端口增加必填标识;创建/编辑均提示并聚焦具体错误字段,端口仅允许 1–65535 整数,不再接受 parseInt 的小数或尾随文本。网络/响应解析失败显示错误并保留表单。
|
||||
- 编辑 API 在修改字段前检查空名称和端口范围;数据库回归验证非法请求不部分保存、合法编辑正常落库。5 项前端测试和针对性后端测试通过。
|
||||
@@ -25,7 +25,7 @@
|
||||
| 20 | — | 量任务已经执行结束,任务管理页也将队列标记为“已完成 100%”,但该任务创建的容器仍长期保持“ | 当前仓库未实现,不扩展能力 |
|
||||
| 21 | 19 | webshell 连接口令明文 | 已修复并有针对性验证 |
|
||||
| 22 | 20 | 不知道这个是啥,应该是样式问题 | 与第 25 行同一 WebShell 路径/假文件问题,已修复 |
|
||||
| 23 | 21 | 创建监听器 必填字段不清楚,交互上不知道是哪个必填 | 待核对 |
|
||||
| 23 | 21 | 创建监听器 必填字段不清楚,交互上不知道是哪个必填 | 已修复必填标识、逐字段提示及端口严格校验;前后端回归通过 |
|
||||
| 24 | 22 | 已经取消的任务,结束之后标签会变成成功,好像没有取消成功 | C2 终态已修复,其他任务路径待核对 |
|
||||
| 25 | 23 | 【WebShell 文件管理】目录树用相对路径导致点进去必报错,且报错文本被解析成假文件条目步骤 | 已修复并有针对性验证 |
|
||||
| 26 | 24 | 工作流 id 由客户端随便填、零校验 | 已修复新 ID 格式/长度、重复创建及编辑改 ID 校验 |
|
||||
@@ -52,7 +52,7 @@
|
||||
| 47 | — | nmap 参数缺少前置校验 | 待核对 |
|
||||
| 48 | — | 对单个资产发起“只读连通性与服务指纹核验”后,Agent 已通过 nmap 和 HTTP 指纹工 | 待核对 |
|
||||
| 49 | — | C2 会话终端执行没有换行结尾的命令时,系统内部用于获取当前目录的 pwd 输出会与用户命令输出 | 已修复,Unix Shell 回归通过 |
|
||||
| 50 | — | 监听器编辑后点击保存没有反应 | 待核对 |
|
||||
| 50 | — | 监听器编辑后点击保存没有反应 | 已修复弹窗事件冒泡阻断;实际委托保存和失败提示回归通过 |
|
||||
| 51 | — | 创建 Malleable Profile 时 UA、响应头和 Jitter 配置被静默丢弃 | 已修复并有针对性验证 |
|
||||
| 52 | — | C2 载荷生成未校验回连地址,非法地址仍能生成 Oneliner | 待核对 |
|
||||
| 53 | — | 知识库索引构建失败原因不怎么太清晰 | 待核对 |
|
||||
|
||||
@@ -177,6 +177,15 @@ func (h *C2Handler) UpdateListener(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Name) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "listener name is required"})
|
||||
return
|
||||
}
|
||||
if err := c2.SafeBindPort(req.BindPort); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 若监听器在运行,不能修改关键字段
|
||||
if h.mgr().IsListenerRunning(id) {
|
||||
if req.BindHost != listener.BindHost || req.BindPort != listener.BindPort {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cyberstrike-ai/internal/c2"
|
||||
"cyberstrike-ai/internal/database"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestC2ListenerEditRejectsInvalidFieldsWithoutSaving(t *testing.T) {
|
||||
db, err := database.NewDB(filepath.Join(t.TempDir(), "listener.db"), zap.NewNop())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
original := &database.C2Listener{ID: "listener", Name: "original", Type: "http_beacon", BindHost: "127.0.0.1", BindPort: 8080, Status: "stopped", CreatedAt: time.Now()}
|
||||
if err := db.CreateC2Listener(original); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := NewC2Handler(c2.NewManager(db, zap.NewNop(), t.TempDir()), zap.NewNop())
|
||||
r := gin.New()
|
||||
r.PUT("/listeners/:id", h.UpdateListener)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
port int
|
||||
status int
|
||||
}{{"", 8080, 400}, {" ", 8080, 400}, {"changed", 0, 400}, {"changed", 65536, 400}, {"changed", 8081, 200}} {
|
||||
body := fmt.Sprintf(`{"name":%q,"bind_host":"127.0.0.1","bind_port":%d,"remark":"new remark"}`, tc.name, tc.port)
|
||||
req := httptest.NewRequest(http.MethodPut, "/listeners/listener", bytes.NewBufferString(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
r.ServeHTTP(w, req)
|
||||
if w.Code != tc.status {
|
||||
t.Fatalf("status %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
saved, err := db.GetC2Listener("listener")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tc.status == 400 && (saved.Name != "original" || saved.BindPort != 8080 || saved.Remark != "") {
|
||||
t.Fatalf("invalid request saved fields: %#v", saved)
|
||||
}
|
||||
if tc.status == 200 && (saved.Name != "changed" || saved.BindPort != 8081 || saved.Remark != "new remark") {
|
||||
t.Fatalf("valid edit not saved: %#v", saved)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4144,6 +4144,10 @@
|
||||
"failed": "Failed"
|
||||
},
|
||||
"listeners": {
|
||||
"nameRequired": "Enter a listener name",
|
||||
"typeRequired": "Select a listener type",
|
||||
"portInvalid": "Port must be an integer from 1 to 65535",
|
||||
"saveFailed": "Failed to save listener",
|
||||
"title": "Listener Management",
|
||||
"create": "Create Listener",
|
||||
"name": "Name",
|
||||
|
||||
@@ -4156,6 +4156,10 @@
|
||||
"failed": "Ошибка"
|
||||
},
|
||||
"listeners": {
|
||||
"nameRequired": "Введите имя слушателя",
|
||||
"typeRequired": "Выберите тип слушателя",
|
||||
"portInvalid": "Порт должен быть целым числом от 1 до 65535",
|
||||
"saveFailed": "Не удалось сохранить слушатель",
|
||||
"title": "Управление слушателями",
|
||||
"create": "Создать слушатель",
|
||||
"name": "Имя",
|
||||
|
||||
@@ -4132,6 +4132,10 @@
|
||||
"failed": "失败"
|
||||
},
|
||||
"listeners": {
|
||||
"nameRequired": "请输入监听器名称",
|
||||
"typeRequired": "请选择监听器类型",
|
||||
"portInvalid": "端口必须为 1–65535 的整数",
|
||||
"saveFailed": "监听器保存失败",
|
||||
"title": "监听器管理",
|
||||
"create": "创建监听器",
|
||||
"name": "名称",
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
const fs = require('node:fs');
|
||||
const vm = require('node:vm');
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const source = fs.readFileSync('web/static/js/c2.js', 'utf8');
|
||||
function harness(overrides = {}, failure) {
|
||||
const fields = Object.fromEntries(Object.entries({ name: 'listener', type: 'http', host: '127.0.0.1', port: '8080', ...overrides }).map(([key, value]) => ['c2-listener-' + key, { value, setAttribute(k, v) { this[k] = v; }, focus() { this.focused = true; }, setCustomValidity(v) { this.message = v; }, reportValidity() {} }]));
|
||||
const requests = [], messages = [];
|
||||
let closed = false;
|
||||
const c = vm.createContext({ document: { getElementById: id => fields[id] }, C2: { listeners: [], closeModal() { closed = true; }, loadListeners() {} }, API_BASE: '/api/c2', c2t: key => key, showToast: (message, type) => messages.push({ message, type }), apiRequest: async (...args) => { requests.push(args); if (failure) throw Error(failure); return {}; } });
|
||||
vm.runInContext(source.slice(source.indexOf(' function validateListenerForm('), source.indexOf(' C2.startListener =')), c);
|
||||
vm.runInContext(source.slice(source.indexOf(' C2.saveListener ='), source.indexOf(' // ============================================================================\n // 会话管理')), c);
|
||||
return { c, fields, requests, messages, closed: () => closed };
|
||||
}
|
||||
test('creation and editing reject invalid fields and identify the field', async () => {
|
||||
for (const method of ['createListener', 'saveListener']) for (const values of [{ name: '' }, { port: '0' }, { port: '65536' }, { port: '1.5' }, { port: '80abc' }]) {
|
||||
const h = harness(values);
|
||||
await h.c.C2[method]('id');
|
||||
assert.equal(h.requests.length, 0);
|
||||
assert.equal(h.messages.length, 1);
|
||||
assert.ok(Object.values(h.fields).some(field => field.focused && field['aria-invalid'] === 'true'));
|
||||
}
|
||||
});
|
||||
test('valid edit sends PUT and closes only after success', async () => {
|
||||
const h = harness();
|
||||
await h.c.C2.saveListener('id');
|
||||
assert.equal(h.requests[0][0], 'PUT');
|
||||
assert.equal(h.requests[0][1], '/api/c2/listeners/id');
|
||||
assert.equal(h.requests[0][2].bind_port, 8080);
|
||||
assert.equal(h.closed(), true);
|
||||
});
|
||||
test('transport failure is visible and keeps editor open', async () => {
|
||||
for (const method of ['createListener', 'saveListener']) {
|
||||
const h = harness({}, 'offline');
|
||||
await h.c.C2[method]('id');
|
||||
assert.equal(h.messages.at(-1).message, 'offline');
|
||||
assert.equal(h.messages.at(-1).type, 'error');
|
||||
assert.equal(h.closed(), false);
|
||||
}
|
||||
});
|
||||
test('modal allows click delegation while background close is target guarded', () => {
|
||||
const html = fs.readFileSync('web/templates/index.html', 'utf8');
|
||||
const modal = html.slice(html.indexOf('<div id="c2-modal"'), html.indexOf('<div id="c2-modal"') + 360);
|
||||
assert.match(modal, /if\(event.target===this\)C2.closeModal\(\)/);
|
||||
assert.doesNotMatch(modal, /stopPropagation/);
|
||||
assert.match(source, /case 'listener-save': C2.saveListener\(id\)/);
|
||||
});
|
||||
test('delegated edit-save button reaches the actual save function', async () => {
|
||||
const h = harness();
|
||||
const handlers = {};
|
||||
h.c.document.documentElement = { dataset: {} };
|
||||
h.c.document.addEventListener = (type, callback) => { handlers[type] = callback; };
|
||||
const start = source.indexOf(' function bindC2SafeActionDelegation(');
|
||||
const end = source.indexOf(' function runC2SafeAction(', start);
|
||||
vm.runInContext(source.slice(start, end), h.c);
|
||||
// Use the original action dispatcher, trimming at its next function boundary.
|
||||
const dispatchStart = source.indexOf(' function runC2SafeAction(');
|
||||
const dispatchEnd = source.indexOf('\n function ', dispatchStart + 10);
|
||||
vm.runInContext(source.slice(dispatchStart, dispatchEnd), h.c);
|
||||
const button = { getAttribute: key => ({ 'data-c2-action': 'listener-save', 'data-c2-id': 'listener-id' })[key] };
|
||||
handlers.click({ target: { closest: selector => selector === '[data-c2-action]' ? button : null }, preventDefault() {}, stopPropagation() {} });
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(h.requests.length, 1);
|
||||
assert.equal(h.requests[0][1], '/api/c2/listeners/listener-id');
|
||||
assert.equal(h.closed(), true);
|
||||
});
|
||||
+35
-14
@@ -1035,11 +1035,11 @@
|
||||
<div class="c2-modal-body">
|
||||
<div class="c2-form-row">
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.name'))}</label>
|
||||
<label>${escapeHtml(c2t('c2.listeners.name'))} <span aria-hidden="true">*</span></label>
|
||||
<input type="text" id="c2-listener-name" class="form-control" placeholder="${escapeAttr(c2t('c2.listeners.placeholderNameExample'))}">
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.type'))}</label>
|
||||
<label>${escapeHtml(c2t('c2.listeners.type'))} <span aria-hidden="true">*</span></label>
|
||||
<select id="c2-listener-type" class="form-control c2-form-select-native">
|
||||
<option value="http_beacon">HTTP Beacon</option>
|
||||
<option value="https_beacon">HTTPS Beacon</option>
|
||||
@@ -1059,7 +1059,7 @@
|
||||
<div class="form-hint">${escapeHtml(c2t('c2.listeners.bindHintExternal'))}</div>
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.bindPort'))}</label>
|
||||
<label>${escapeHtml(c2t('c2.listeners.bindPort'))} <span aria-hidden="true">*</span></label>
|
||||
<input type="number" id="c2-listener-port" class="form-control" placeholder="8443">
|
||||
</div>
|
||||
</div>
|
||||
@@ -1121,7 +1121,32 @@
|
||||
syncC2FormSelect('c2-listener-profile-id');
|
||||
};
|
||||
|
||||
function validateListenerForm(creating) {
|
||||
const checks = [
|
||||
['c2-listener-name', 'c2.listeners.nameRequired', value => !!value.trim()],
|
||||
...(creating ? [['c2-listener-type', 'c2.listeners.typeRequired', value => !!value]] : []),
|
||||
['c2-listener-port', 'c2.listeners.portInvalid', value => /^\d+$/.test(value) && Number(value) >= 1 && Number(value) <= 65535]
|
||||
];
|
||||
let firstInvalid;
|
||||
checks.forEach(([id, key, valid]) => {
|
||||
const input = document.getElementById(id);
|
||||
if (!input) return;
|
||||
const invalid = !valid(String(input.value || ''));
|
||||
input.setAttribute('aria-invalid', String(invalid));
|
||||
input.setCustomValidity?.(invalid ? c2t(key) : '');
|
||||
if (invalid && !firstInvalid) firstInvalid = { input, key };
|
||||
});
|
||||
if (firstInvalid) {
|
||||
showToast(c2t(firstInvalid.key), 'error');
|
||||
firstInvalid.input.focus();
|
||||
firstInvalid.input.reportValidity?.();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
C2.createListener = function() {
|
||||
if (!validateListenerForm(true)) return;
|
||||
const name = document.getElementById('c2-listener-name')?.value.trim();
|
||||
const type = document.getElementById('c2-listener-type')?.value;
|
||||
const bindHost = document.getElementById('c2-listener-host')?.value || '127.0.0.1';
|
||||
@@ -1129,11 +1154,6 @@
|
||||
const callbackHost = document.getElementById('c2-listener-callback-host')?.value?.trim() || '';
|
||||
const remark = document.getElementById('c2-listener-remark')?.value;
|
||||
|
||||
if (!name || !type || !bindPort) {
|
||||
showToast(c2t('c2.listeners.toastFillRequired'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const profileId = (document.getElementById('c2-listener-profile-id')?.value || '').trim();
|
||||
const legacyShell = document.getElementById('c2-listener-legacy-shell')?.checked === true;
|
||||
const body = {
|
||||
@@ -1146,7 +1166,7 @@
|
||||
body.config = { allow_legacy_shell: true };
|
||||
}
|
||||
|
||||
apiRequest('POST', `${API_BASE}/listeners`, body).then(data => {
|
||||
return apiRequest('POST', `${API_BASE}/listeners`, body).then(data => {
|
||||
if (data.error) {
|
||||
showToast(data.error, 'error');
|
||||
} else {
|
||||
@@ -1154,7 +1174,7 @@
|
||||
C2.closeModal();
|
||||
C2.loadListeners();
|
||||
}
|
||||
});
|
||||
}).catch(error => showToast(error.message || c2t('c2.listeners.saveFailed'), 'error'));
|
||||
};
|
||||
|
||||
C2.startListener = function(id) {
|
||||
@@ -1263,7 +1283,7 @@
|
||||
</div>
|
||||
<div class="c2-modal-body">
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.name'))}</label>
|
||||
<label>${escapeHtml(c2t('c2.listeners.name'))} <span aria-hidden="true">*</span></label>
|
||||
<input type="text" id="c2-listener-name" class="form-control" value="${escapeAttr(l.name)}">
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
@@ -1276,7 +1296,7 @@
|
||||
<input type="text" id="c2-listener-host" class="form-control" value="${escapeAttr(String(l.bindHost))}">
|
||||
</div>
|
||||
<div class="c2-form-group">
|
||||
<label>${escapeHtml(c2t('c2.listeners.bindPort'))}</label>
|
||||
<label>${escapeHtml(c2t('c2.listeners.bindPort'))} <span aria-hidden="true">*</span></label>
|
||||
<input type="number" id="c2-listener-port" class="form-control" value="${l.bindPort}">
|
||||
</div>
|
||||
</div>
|
||||
@@ -1317,6 +1337,7 @@
|
||||
};
|
||||
|
||||
C2.saveListener = function(id) {
|
||||
if (!validateListenerForm(false)) return;
|
||||
const name = document.getElementById('c2-listener-name')?.value.trim();
|
||||
const bindHost = document.getElementById('c2-listener-host')?.value;
|
||||
const bindPort = parseInt(document.getElementById('c2-listener-port')?.value);
|
||||
@@ -1339,14 +1360,14 @@
|
||||
body.config = merged;
|
||||
}
|
||||
|
||||
apiRequest('PUT', `${API_BASE}/listeners/${id}`, body).then(data => {
|
||||
return apiRequest('PUT', `${API_BASE}/listeners/${id}`, body).then(data => {
|
||||
if (data.error) showToast(data.error, 'error');
|
||||
else {
|
||||
showToast(c2t('c2.listeners.toastUpdated'), 'success');
|
||||
C2.closeModal();
|
||||
C2.loadListeners();
|
||||
}
|
||||
});
|
||||
}).catch(error => showToast(error.message || c2t('c2.listeners.saveFailed'), 'error'));
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -3249,7 +3249,7 @@
|
||||
|
||||
<!-- C2 模态框 -->
|
||||
<div id="c2-modal" class="c2-modal-overlay" style="display:none;" onclick="if(event.target===this)C2.closeModal()">
|
||||
<div class="c2-modal" onclick="event.stopPropagation()">
|
||||
<div class="c2-modal">
|
||||
<div id="c2-modal-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user