Skip to content

Commit 0d31374

Browse files
committed
fix: Deep audit — fix broken dispatch URLs, state machine gaps, and input validation
- Fix config.port (undefined) → config.metricsPort in all webhook dispatch messages - Add closed as terminal state in status transitions (no transitions allowed) - Mark plans as FAILED when all actions denied by policy (not COMPLETED) - Add status and risk_level validation on agent-action endpoints - Sanitize data param with field allowlist to prevent arbitrary injection - Add webhook dispatch for planned→policy-guard and approved→responder - Add edit/write/exec to installer global tools.allow list - Standardize asset criticality tables across policy-guard and correlation - Add MITRE TA0011 (Command and Control) to correlation kill chain - Fix listPlans unguarded getPlan throw, callMcpTool lastError guard - Allow dots in MCP tool name regex, handle link-local/CGNAT in isPrivateIp - Drain unconsumed fetch response bodies in MCP init and webhook dispatch
1 parent ae46db0 commit 0d31374

File tree

5 files changed

+54
-17
lines changed

5 files changed

+54
-17
lines changed

install/install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -967,7 +967,7 @@ deploy_agents() {
967967
968968
"tools": {
969969
"profile": "minimal",
970-
"allow": ["read", "web_fetch", "sessions_list", "sessions_history", "sessions_send"],
970+
"allow": ["read", "edit", "write", "exec", "web_fetch", "sessions_list", "sessions_history", "sessions_send"],
971971
"deny": ["browser", "canvas"],
972972
"web": {
973973
"search": {"enabled": false},

openclaw/agents/correlation/AGENTS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ Build chronological timelines and tag each event with its MITRE kill chain phase
111111
8. Lateral Movement (TA0008)
112112
9. Collection (TA0009)
113113
10. Exfiltration (TA0010)
114-
11. Impact (TA0040)
114+
11. Command and Control (TA0011)
115+
12. Impact (TA0040)
115116

116117
The presence of 3+ kill chain phases in a single cluster is a strong indicator of a coordinated attack and should boost the overall severity.
117118

@@ -149,8 +150,8 @@ Classify hostnames by regex match:
149150
|---|---|
150151
| `^dc-\|^ad-\|^ldap-` | CRITICAL |
151152
| `^prod-\|^prd-` | HIGH |
152-
| `^db-\|^sql-\|^mongo-\|^redis-` | HIGH |
153-
| `^app-\|^web-\|^api-` | HIGH |
153+
| `^db-\|^sql-\|^mongo-\|^redis-\|^elastic-` | HIGH |
154+
| `^app-\|^web-\|^api-` | MEDIUM |
154155
| `^stage-\|^staging-\|^stg-` | MEDIUM |
155156
| `^dev-\|^test-\|^sandbox-` | LOW |
156157

openclaw/agents/policy-guard/AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@ Characteristics: Enterprise blast radius, severe service impact
6565
|---------|-------------|------------------|
6666
| `^(dc\|ad\|ldap)-.*` | Critical | Admin |
6767
| `^(prod\|prd)-.*` | High | Elevated |
68-
| `^(db\|sql\|mongo\|redis)-.*` | High | Elevated |
69-
| `^(staging\|stg)-.*` | Medium | Standard |
68+
| `^(db\|sql\|mongo\|redis\|elastic)-.*` | High | Elevated |
69+
| `^(app\|web\|api)-.*` | Medium | Elevated |
70+
| `^(staging\|stg\|stage)-.*` | Medium | Standard |
7071
| `^(dev\|test\|sandbox)-.*` | Low | Standard |
7172

7273
## Privileged User Patterns

openclaw/agents/triage/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ Emit a JSON object for each triaged alert. Example:
160160
{"value": "admin", "type": "target", "privileged": true}
161161
],
162162
"hosts": [
163-
{"value": "prod-web-01", "criticality": "critical", "os": "linux"}
163+
{"value": "prod-web-01", "criticality": "high", "os": "linux"}
164164
],
165165
"processes": [],
166166
"hashes": [],

runtime/autopilot-service/index.js

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ async function dispatchToGateway(webhookPath, payload) {
299299
clearTimeout(timeoutId);
300300

301301
if (response.ok) {
302+
await response.text().catch(() => ""); // drain response body
302303
incrementMetric("webhook_dispatches_total");
303304
log("info", "dispatch", "Webhook dispatched", {
304305
path: webhookPath,
@@ -535,7 +536,7 @@ async function _doMcpSessionInit() {
535536
const controller2 = new AbortController();
536537
const timeoutId2 = setTimeout(() => controller2.abort(), 5000);
537538

538-
await fetch(`${config.mcpUrl}/mcp`, {
539+
const notifResp = await fetch(`${config.mcpUrl}/mcp`, {
539540
method: "POST",
540541
headers: {
541542
"Content-Type": "application/json",
@@ -549,6 +550,7 @@ async function _doMcpSessionInit() {
549550
}),
550551
signal: controller2.signal,
551552
});
553+
await notifResp.text().catch(() => ""); // drain response body
552554

553555
clearTimeout(timeoutId2);
554556

@@ -591,6 +593,10 @@ function isPrivateIp(ip) {
591593
if (a === 127) return true;
592594
// 0.0.0.0/8
593595
if (a === 0) return true;
596+
// 169.254.0.0/16 (link-local / APIPA)
597+
if (a === 169 && b === 254) return true;
598+
// 100.64.0.0/10 (CGNAT / Tailscale)
599+
if (a === 100 && b >= 64 && b <= 127) return true;
594600
return false;
595601
}
596602

@@ -894,6 +900,7 @@ async function updateCase(caseId, updates) {
894900
executed: ["closed", "false_positive"],
895901
rejected: ["open", "closed"],
896902
false_positive: ["open"],
903+
closed: [], // Terminal state — no transitions allowed
897904
};
898905
const currentStatus = evidencePack.status || "open";
899906
const allowed = VALID_STATUS_TRANSITIONS[currentStatus];
@@ -980,13 +987,17 @@ async function updateCase(caseId, updates) {
980987
triaged: "/webhook/case-created", // → correlation agent
981988
correlated: "/webhook/investigation-request", // → investigation agent
982989
investigated: "/webhook/plan-request", // → response-planner agent
990+
planned: "/webhook/policy-check", // → policy-guard agent
991+
approved: "/webhook/execute-action", // → responder agent
983992
};
984993
const webhookPath = statusWebhooks[updates.status];
985994
if (webhookPath) {
986995
const statusMessages = {
987-
triaged: `Correlate case ${caseId} (${evidencePack.severity} severity). Search for related alerts, identify attack patterns, then use web_fetch to call: http://localhost:${config.port}/api/agent-action/update-case?case_id=${caseId}&status=correlated`,
988-
correlated: `Investigate case ${caseId} (${evidencePack.severity} severity). Perform deep analysis using MCP tools: check agent health, search security events, analyze threat indicators. Then use web_fetch to call: http://localhost:${config.port}/api/agent-action/update-case?case_id=${caseId}&status=investigated`,
989-
investigated: `Plan response for case ${caseId} (${evidencePack.severity} severity). Review investigation findings and create a response plan. Then use web_fetch to submit the plan: http://localhost:${config.port}/api/agent-action/create-plan?case_id=${caseId}&title={url_encoded_title}&risk_level={risk_level}&actions={url_encoded_actions_json}`,
996+
triaged: `Correlate case ${caseId} (${evidencePack.severity} severity). Search for related alerts, identify attack patterns, then use web_fetch to call: http://localhost:${config.metricsPort}/api/agent-action/update-case?case_id=${caseId}&status=correlated`,
997+
correlated: `Investigate case ${caseId} (${evidencePack.severity} severity). Perform deep analysis using MCP tools: check agent health, search security events, analyze threat indicators. Then use web_fetch to call: http://localhost:${config.metricsPort}/api/agent-action/update-case?case_id=${caseId}&status=investigated`,
998+
investigated: `Plan response for case ${caseId} (${evidencePack.severity} severity). Review investigation findings and create a response plan. Then use web_fetch to submit the plan: http://localhost:${config.metricsPort}/api/agent-action/create-plan?case_id=${caseId}&title={url_encoded_title}&risk_level={risk_level}&actions={url_encoded_actions_json}`,
999+
planned: `Evaluate proposed plan for case ${caseId} (${evidencePack.severity} severity). Check all policy rules, risk levels, and approval requirements. Then use web_fetch to submit your decision: http://localhost:${config.metricsPort}/api/agent-action/approve-plan?plan_id={plan_id}&approver_id=policy-guard&decision={allow|deny|escalate}&reason={url_encoded_reason}`,
1000+
approved: `Execute approved plan for case ${caseId} (${evidencePack.severity} severity). Check responder status, then execute the plan. Use web_fetch to call: http://localhost:${config.metricsPort}/api/agent-action/execute-plan?plan_id={plan_id}&executor_id=responder-agent`,
9901001
};
9911002
dispatchToGateway(webhookPath, {
9921003
message: statusMessages[updates.status] || `Process case ${caseId} — status changed to ${updates.status}.`,
@@ -1335,8 +1346,12 @@ function listPlans(options = {}) {
13351346

13361347
for (const [planId, plan] of responsePlans.entries()) {
13371348
// Trigger expiry check via getPlan (updates stale PROPOSED/APPROVED → EXPIRED)
1338-
const freshPlan = getPlan(planId, { updateExpiry: true });
1339-
if (!freshPlan) continue;
1349+
let freshPlan;
1350+
try {
1351+
freshPlan = getPlan(planId, { updateExpiry: true });
1352+
} catch {
1353+
continue; // Plan was deleted during iteration
1354+
}
13401355
// Filter by state (after expiry update)
13411356
if (state && freshPlan.state !== state) continue;
13421357
// Filter by case
@@ -1519,6 +1534,7 @@ async function executePlan(planId, executorId) {
15191534
// Policy enforcement: idempotency check
15201535
const idempResult = policyCheckIdempotency(action.type, action.target);
15211536
if (!idempResult.allowed) {
1537+
allSuccess = false;
15221538
incrementMetric("policy_denies_total", { reason: "duplicate_action", action: action.type });
15231539
log("warn", "policy", "Action denied by idempotency check", {
15241540
plan_id: planId, action_type: action.type, target: action.target, reason: idempResult.reason,
@@ -1536,6 +1552,7 @@ async function executePlan(planId, executorId) {
15361552
// Policy enforcement: rate limit check
15371553
const rlResult = policyCheckActionRateLimit(action.type);
15381554
if (!rlResult.allowed) {
1555+
allSuccess = false;
15391556
const rlReason = rlResult.reason.includes("global") ? "global_rate_limited" : "action_rate_limited";
15401557
incrementMetric("policy_denies_total", { reason: rlReason, action: action.type });
15411558
log("warn", "policy", "Action denied by rate limit", {
@@ -2273,7 +2290,7 @@ async function callMcpTool(toolName, params, correlationId) {
22732290
const mcpToolName = resolveMcpTool(toolName);
22742291

22752292
// Validate tool name to prevent path traversal (SSRF)
2276-
if (!/^[a-zA-Z0-9_-]+$/.test(mcpToolName)) {
2293+
if (!/^[a-zA-Z0-9_.\-]+$/.test(mcpToolName)) {
22772294
incrementMetric("errors_total", { component: "mcp" });
22782295
throw new Error(`Invalid tool name: contains disallowed characters`);
22792296
}
@@ -2443,7 +2460,7 @@ async function callMcpTool(toolName, params, correlationId) {
24432460
}
24442461

24452462
// All attempts exhausted — skip metric recording if already done (e.g., RPC errors)
2446-
if (!lastError._metricsRecorded) {
2463+
if (lastError && !lastError._metricsRecorded) {
24472464
const latencySeconds = (Date.now() - startTime) / 1000;
24482465
incrementMetric("mcp_tool_calls_total", { tool: toolName, status: "error" });
24492466
incrementMetric("errors_total", { component: "mcp" });
@@ -3279,7 +3296,7 @@ function createServer() {
32793296

32803297
// Dispatch to triage agent via OpenClaw gateway
32813298
dispatchToGateway("/webhook/wazuh-alert", {
3282-
message: `Triage new ${severity}-severity alert: ${caseData.title}. Case ${caseId} with ${entities.length} entities extracted. Analyze the alert, assess threat level, then use web_fetch to call: http://localhost:${config.port}/api/agent-action/update-case?case_id=${caseId}&status=triaged`,
3299+
message: `Triage new ${severity}-severity alert: ${caseData.title}. Case ${caseId} with ${entities.length} entities extracted. Analyze the alert, assess threat level, then use web_fetch to call: http://localhost:${config.metricsPort}/api/agent-action/update-case?case_id=${caseId}&status=triaged`,
32833300
case_id: caseId,
32843301
severity,
32853302
title: caseData.title,
@@ -3346,11 +3363,23 @@ function createServer() {
33463363
return;
33473364
}
33483365

3366+
const VALID_AGENT_STATUSES = ["triaged", "correlated", "investigated", "planned", "approved", "executed", "closed", "false_positive"];
3367+
if (status && !VALID_AGENT_STATUSES.includes(status)) {
3368+
sendJsonError(res, 400, `Invalid status: must be one of ${VALID_AGENT_STATUSES.join(", ")}`, requestId);
3369+
return;
3370+
}
3371+
3372+
const ALLOWED_DATA_FIELDS = ["title", "summary", "severity", "confidence", "correlation", "findings", "recommended_response", "iocs", "enrichment_data", "mitre", "entities", "timeline"];
33493373
const updates = {};
33503374
if (status) updates.status = status;
33513375
if (dataParam) {
33523376
try {
3353-
Object.assign(updates, JSON.parse(dataParam));
3377+
const parsed = JSON.parse(dataParam);
3378+
for (const key of Object.keys(parsed)) {
3379+
if (ALLOWED_DATA_FIELDS.includes(key)) {
3380+
updates[key] = parsed[key];
3381+
}
3382+
}
33543383
} catch {
33553384
sendJsonError(res, 400, "Invalid JSON in data parameter", requestId);
33563385
return;
@@ -3393,6 +3422,12 @@ function createServer() {
33933422
const riskLevel = url.searchParams.get("risk_level") || "medium";
33943423
const actionsParam = url.searchParams.get("actions");
33953424

3425+
const VALID_RISK_LEVELS = ["low", "medium", "high", "critical"];
3426+
if (!VALID_RISK_LEVELS.includes(riskLevel)) {
3427+
sendJsonError(res, 400, `Invalid risk_level: must be one of ${VALID_RISK_LEVELS.join(", ")}`, requestId);
3428+
return;
3429+
}
3430+
33963431
if (!caseId || !isValidCaseId(caseId)) {
33973432
sendJsonError(res, 400, "Invalid or missing case_id", requestId);
33983433
return;

0 commit comments

Comments
 (0)