@drakon-systems/shieldcortex-realtime 4.32.1 → 4.32.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cloud-sync.ts +23 -9
- package/dist/audit-entry.js +56 -0
- package/dist/cloud-sync.js +23 -9
- package/dist/index.js +3 -2
- package/dist/intercept-ingest.js +27 -10
- package/dist/interceptor.js +31 -9
- package/dist/openclaw.plugin.json +1 -1
- package/index.ts +3 -2
- package/intercept-ingest.ts +25 -11
- package/interceptor.ts +42 -9
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
package/cloud-sync.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// plugins/openclaw/cloud-sync.ts
|
|
2
2
|
//
|
|
3
|
-
// Network egress for SC threat events. See CHANGELOG.md
|
|
3
|
+
// Network egress for SC realtime input-scan threat events. See CHANGELOG.md
|
|
4
|
+
// v4.12.8 / v4.12.9. Posts canonical audit entries to /v1/audit/ingest — the
|
|
5
|
+
// only ingest route the SaaS exposes (the old /v1/threats route never existed
|
|
6
|
+
// server-side, so these events were silently 404'd and dropped).
|
|
7
|
+
|
|
8
|
+
import { toAuditEntry } from './audit-entry.js';
|
|
4
9
|
|
|
5
10
|
type CloudSyncConfig = {
|
|
6
11
|
cloudEnabled?: boolean;
|
|
@@ -15,20 +20,29 @@ export function cloudSync(threat: Record<string, unknown>, cfg: CloudSyncConfig)
|
|
|
15
20
|
// cloud sync was "off" (ClawScan finding: external scanning without a local-only
|
|
16
21
|
// default).
|
|
17
22
|
if (!cfg.cloudEnabled || !cfg.cloudApiKey) return;
|
|
18
|
-
// Privacy: never transmit raw LLM input.
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
23
|
+
// Privacy: never transmit raw LLM input. We build a fresh canonical entry from
|
|
24
|
+
// named METADATA fields only — content/preview are never read or copied, so they
|
|
25
|
+
// cannot leak even though the caller's `threat` object carries them (ClawScan
|
|
26
|
+
// finding: previews may contain credentials or confidential data). The explicit
|
|
27
|
+
// field-passing below IS the privacy boundary. The local audit log keeps fuller
|
|
28
|
+
// detail for triage.
|
|
29
|
+
const entry = toAuditEntry({
|
|
30
|
+
kind: 'realtime',
|
|
31
|
+
hook: typeof threat.hook === 'string' ? threat.hook : undefined,
|
|
32
|
+
sessionId: typeof threat.sessionId === 'string' ? threat.sessionId : undefined,
|
|
33
|
+
model: typeof threat.model === 'string' ? threat.model : undefined,
|
|
34
|
+
reason: typeof threat.reason === 'string' ? threat.reason : undefined,
|
|
35
|
+
ts: typeof threat.ts === 'string' ? threat.ts : undefined,
|
|
36
|
+
});
|
|
37
|
+
if (!entry) return;
|
|
38
|
+
const url = `${cfg.cloudBaseUrl || 'https://api.shieldcortex.ai'}/v1/audit/ingest`;
|
|
25
39
|
fetch(url, {
|
|
26
40
|
method: 'POST',
|
|
27
41
|
headers: {
|
|
28
42
|
'Content-Type': 'application/json',
|
|
29
43
|
Authorization: `Bearer ${cfg.cloudApiKey}`,
|
|
30
44
|
},
|
|
31
|
-
body: JSON.stringify(
|
|
45
|
+
body: JSON.stringify({ entries: [entry] }),
|
|
32
46
|
signal: AbortSignal.timeout(5000),
|
|
33
47
|
}).catch(() => {
|
|
34
48
|
// Fire-and-forget — never block on cloud sync failure
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// plugins/openclaw/audit-entry.ts
|
|
2
|
+
// Canonical audit-entry builder for the OpenClaw plugin's cloud egress.
|
|
3
|
+
// Mirrors the SaaS ingestSchema (ShieldCortex-internal/src/routes/v1/audit.ts).
|
|
4
|
+
// Privacy: callers pass METADATA only — this never reads content/preview.
|
|
5
|
+
function clamp01(n) {
|
|
6
|
+
const v = typeof n === 'number' && Number.isFinite(n) ? n : 0;
|
|
7
|
+
return Math.max(0, Math.min(1, v));
|
|
8
|
+
}
|
|
9
|
+
function normalizeFirewallResult(raw, fallback) {
|
|
10
|
+
const s = String(raw ?? '').toUpperCase();
|
|
11
|
+
if (s === 'ALLOW' || s === 'BLOCK' || s === 'QUARANTINE')
|
|
12
|
+
return s;
|
|
13
|
+
if (s === 'DENY' || s === 'DENIED' || s === 'AUTO_DENIED' || s === 'BLOCKED')
|
|
14
|
+
return 'BLOCK';
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
function isoTimestamp(raw) {
|
|
18
|
+
const c = typeof raw === 'string' ? raw : '';
|
|
19
|
+
const t = c ? new Date(c).getTime() : NaN;
|
|
20
|
+
return Number.isNaN(t) ? new Date().toISOString() : new Date(t).toISOString();
|
|
21
|
+
}
|
|
22
|
+
export function toAuditEntry(input) {
|
|
23
|
+
if (!input)
|
|
24
|
+
return null;
|
|
25
|
+
const timestamp = isoTimestamp(input.ts);
|
|
26
|
+
if (input.kind === 'intercept') {
|
|
27
|
+
const anomaly = clamp01(input.anomalyScore);
|
|
28
|
+
return {
|
|
29
|
+
source_type: 'openclaw-interceptor',
|
|
30
|
+
source_identifier: input.tool || 'openclaw',
|
|
31
|
+
trust_score: input.trustScore != null ? clamp01(input.trustScore) : clamp01(1 - anomaly),
|
|
32
|
+
sensitivity_level: input.sensitivityLevel || 'INTERNAL',
|
|
33
|
+
firewall_result: normalizeFirewallResult(input.firewallResult, 'QUARANTINE'),
|
|
34
|
+
anomaly_score: anomaly,
|
|
35
|
+
threat_indicators: Array.isArray(input.threats) ? input.threats.map(String) : [],
|
|
36
|
+
fragmentation_score: input.fragmentationScore == null ? null : clamp01(input.fragmentationScore),
|
|
37
|
+
reason: `OpenClaw intercept: ${input.tool ?? 'tool'} → ${input.outcome ?? input.action ?? 'logged'}`,
|
|
38
|
+
pipeline_duration_ms: Math.max(0, Math.trunc(input.pipelineDurationMs ?? 0)),
|
|
39
|
+
timestamp,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const reason = input.reason || 'OpenClaw realtime threat';
|
|
43
|
+
return {
|
|
44
|
+
source_type: input.hook || 'openclaw-realtime',
|
|
45
|
+
source_identifier: input.model || input.sessionId || 'openclaw',
|
|
46
|
+
trust_score: 0.5,
|
|
47
|
+
sensitivity_level: 'INTERNAL',
|
|
48
|
+
firewall_result: 'QUARANTINE',
|
|
49
|
+
anomaly_score: 0,
|
|
50
|
+
threat_indicators: [reason],
|
|
51
|
+
fragmentation_score: null,
|
|
52
|
+
reason,
|
|
53
|
+
pipeline_duration_ms: 0,
|
|
54
|
+
timestamp,
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/cloud-sync.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// plugins/openclaw/cloud-sync.ts
|
|
2
2
|
//
|
|
3
|
-
// Network egress for SC threat events. See CHANGELOG.md
|
|
3
|
+
// Network egress for SC realtime input-scan threat events. See CHANGELOG.md
|
|
4
|
+
// v4.12.8 / v4.12.9. Posts canonical audit entries to /v1/audit/ingest — the
|
|
5
|
+
// only ingest route the SaaS exposes (the old /v1/threats route never existed
|
|
6
|
+
// server-side, so these events were silently 404'd and dropped).
|
|
7
|
+
import { toAuditEntry } from './audit-entry.js';
|
|
4
8
|
export function cloudSync(threat, cfg) {
|
|
5
9
|
// Consent gate: require cloud explicitly enabled AND an API key — matching every
|
|
6
10
|
// other egress sender (intercept-ingest.ts, src/cloud/*). Previously this checked
|
|
@@ -9,20 +13,30 @@ export function cloudSync(threat, cfg) {
|
|
|
9
13
|
// default).
|
|
10
14
|
if (!cfg.cloudEnabled || !cfg.cloudApiKey)
|
|
11
15
|
return;
|
|
12
|
-
// Privacy: never transmit raw LLM input.
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
16
|
+
// Privacy: never transmit raw LLM input. We build a fresh canonical entry from
|
|
17
|
+
// named METADATA fields only — content/preview are never read or copied, so they
|
|
18
|
+
// cannot leak even though the caller's `threat` object carries them (ClawScan
|
|
19
|
+
// finding: previews may contain credentials or confidential data). The explicit
|
|
20
|
+
// field-passing below IS the privacy boundary. The local audit log keeps fuller
|
|
21
|
+
// detail for triage.
|
|
22
|
+
const entry = toAuditEntry({
|
|
23
|
+
kind: 'realtime',
|
|
24
|
+
hook: typeof threat.hook === 'string' ? threat.hook : undefined,
|
|
25
|
+
sessionId: typeof threat.sessionId === 'string' ? threat.sessionId : undefined,
|
|
26
|
+
model: typeof threat.model === 'string' ? threat.model : undefined,
|
|
27
|
+
reason: typeof threat.reason === 'string' ? threat.reason : undefined,
|
|
28
|
+
ts: typeof threat.ts === 'string' ? threat.ts : undefined,
|
|
29
|
+
});
|
|
30
|
+
if (!entry)
|
|
31
|
+
return;
|
|
32
|
+
const url = `${cfg.cloudBaseUrl || 'https://api.shieldcortex.ai'}/v1/audit/ingest`;
|
|
19
33
|
fetch(url, {
|
|
20
34
|
method: 'POST',
|
|
21
35
|
headers: {
|
|
22
36
|
'Content-Type': 'application/json',
|
|
23
37
|
Authorization: `Bearer ${cfg.cloudApiKey}`,
|
|
24
38
|
},
|
|
25
|
-
body: JSON.stringify(
|
|
39
|
+
body: JSON.stringify({ entries: [entry] }),
|
|
26
40
|
signal: AbortSignal.timeout(5000),
|
|
27
41
|
}).catch(() => {
|
|
28
42
|
// Fire-and-forget — never block on cloud sync failure
|
package/dist/index.js
CHANGED
|
@@ -562,8 +562,9 @@ export async function scanLlmInput(event, _ctx) {
|
|
|
562
562
|
};
|
|
563
563
|
auditLog(entry);
|
|
564
564
|
loadConfig()
|
|
565
|
-
// Pass the local entry as-is; cloudSync
|
|
566
|
-
//
|
|
565
|
+
// Pass the local entry as-is; cloudSync rebuilds a canonical metadata-only
|
|
566
|
+
// entry from named fields and never reads preview/content. No raw LLM input
|
|
567
|
+
// leaves here.
|
|
567
568
|
.then(cfg => cloudSync(entry, cfg))
|
|
568
569
|
.catch(() => { });
|
|
569
570
|
}
|
package/dist/intercept-ingest.js
CHANGED
|
@@ -1,21 +1,38 @@
|
|
|
1
|
+
// plugins/openclaw/intercept-ingest.ts
|
|
2
|
+
import { toAuditEntry } from './audit-entry.js';
|
|
1
3
|
export function syncInterceptEvent(event, config) {
|
|
2
4
|
if (!config.cloudEnabled || !config.cloudApiKey)
|
|
3
5
|
return;
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
// Privacy: build a canonical audit entry from METADATA only — the content
|
|
7
|
+
// preview never leaves the machine (ClawScan finding: previews may contain
|
|
8
|
+
// credentials or confidential data). The local audit JSONL retains the
|
|
9
|
+
// preview for triage. toAuditEntry has no notion of preview/content.
|
|
10
|
+
const entry = toAuditEntry({
|
|
11
|
+
kind: 'intercept',
|
|
12
|
+
tool: event.tool,
|
|
13
|
+
firewallResult: event.firewallResult,
|
|
14
|
+
threats: event.threats,
|
|
15
|
+
anomalyScore: event.anomalyScore,
|
|
16
|
+
trustScore: event.trustScore,
|
|
17
|
+
sensitivityLevel: event.sensitivityLevel,
|
|
18
|
+
fragmentationScore: event.fragmentationScore,
|
|
19
|
+
outcome: event.outcome,
|
|
20
|
+
action: event.action,
|
|
21
|
+
pipelineDurationMs: event.pipelineDurationMs,
|
|
22
|
+
ts: event.ts,
|
|
23
|
+
});
|
|
24
|
+
if (!entry)
|
|
25
|
+
return;
|
|
26
|
+
// SaaS /v1/audit/ingest requires { entries: [<canonical snake_case entry>] }
|
|
27
|
+
// (zod ingestSchema). The old { events: [...] } shape was rejected 400 and
|
|
28
|
+
// every interceptor POST was silently dropped.
|
|
29
|
+
fetch(`${config.cloudBaseUrl}/v1/audit/ingest`, {
|
|
11
30
|
method: 'POST',
|
|
12
31
|
headers: {
|
|
13
32
|
'Content-Type': 'application/json',
|
|
14
33
|
Authorization: `Bearer ${config.cloudApiKey}`,
|
|
15
34
|
},
|
|
16
|
-
body: JSON.stringify({
|
|
17
|
-
events: [{ ...metadata, source: 'openclaw-interceptor' }],
|
|
18
|
-
}),
|
|
35
|
+
body: JSON.stringify({ entries: [entry] }),
|
|
19
36
|
signal: AbortSignal.timeout(5_000),
|
|
20
37
|
}).catch(() => {
|
|
21
38
|
// Fire-and-forget — never block on cloud sync failure
|
package/dist/interceptor.js
CHANGED
|
@@ -211,7 +211,10 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
211
211
|
const xrayEntry = {
|
|
212
212
|
type: 'intercept', tool: context.toolName, severity: 'critical',
|
|
213
213
|
firewallResult: 'BLOCK', threats: xrayResult.findings.map(f => f.category),
|
|
214
|
-
anomalyScore: 1,
|
|
214
|
+
anomalyScore: 1,
|
|
215
|
+
// X-Ray short-circuits before the pipeline runs — no pipeline result.
|
|
216
|
+
trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
|
|
217
|
+
action: 'auto_deny', outcome: 'auto_denied',
|
|
215
218
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
216
219
|
};
|
|
217
220
|
emitAudit(xrayEntry);
|
|
@@ -221,12 +224,21 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
221
224
|
let firewallResult;
|
|
222
225
|
let threats;
|
|
223
226
|
let anomalyScore;
|
|
227
|
+
let trustScore;
|
|
228
|
+
let sensitivityLevel;
|
|
229
|
+
let fragmentationScore;
|
|
230
|
+
let pipelineDurationMs;
|
|
224
231
|
try {
|
|
232
|
+
const pipelineStart = Date.now();
|
|
225
233
|
const result = pipeline(content, title, { type: 'agent', identifier: 'openclaw' });
|
|
234
|
+
pipelineDurationMs = Date.now() - pipelineStart;
|
|
226
235
|
severity = mapSeverity(result.firewall);
|
|
227
236
|
firewallResult = result.firewall.result;
|
|
228
237
|
threats = result.firewall.threatIndicators;
|
|
229
238
|
anomalyScore = result.firewall.anomalyScore;
|
|
239
|
+
trustScore = result.trust.score;
|
|
240
|
+
sensitivityLevel = result.sensitivity.level;
|
|
241
|
+
fragmentationScore = result.fragmentation?.score ?? null;
|
|
230
242
|
}
|
|
231
243
|
catch (err) {
|
|
232
244
|
log.warn(`[shieldcortex] ⚠️ Defence pipeline error: ${err instanceof Error ? err.message : err}`);
|
|
@@ -234,6 +246,8 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
234
246
|
const entry = {
|
|
235
247
|
type: 'intercept', tool: context.toolName, severity: 'high',
|
|
236
248
|
firewallResult: 'ERROR', threats: ['pipeline_error'], anomalyScore: 0,
|
|
249
|
+
// Pipeline threw — no result in scope, use documented defaults.
|
|
250
|
+
trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
|
|
237
251
|
action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
|
|
238
252
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
239
253
|
};
|
|
@@ -246,7 +260,8 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
246
260
|
if (denyCache.isDenied(context.toolName, fullContent)) {
|
|
247
261
|
const entry = {
|
|
248
262
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
249
|
-
threats, anomalyScore,
|
|
263
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
264
|
+
action: 'auto_deny', outcome: 'auto_denied',
|
|
250
265
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
251
266
|
};
|
|
252
267
|
emitAudit(entry);
|
|
@@ -256,7 +271,8 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
256
271
|
if (action === 'log') {
|
|
257
272
|
const entry = {
|
|
258
273
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
259
|
-
threats, anomalyScore,
|
|
274
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
275
|
+
action: 'log', outcome: 'logged',
|
|
260
276
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
261
277
|
};
|
|
262
278
|
emitAudit(entry);
|
|
@@ -266,7 +282,8 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
266
282
|
log.warn(`[shieldcortex] ⚠️ ${severity} risk in ${context.toolName}: ${threats.join(', ') || 'anomaly detected'}`);
|
|
267
283
|
const entry = {
|
|
268
284
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
269
|
-
threats, anomalyScore,
|
|
285
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
286
|
+
action: 'warn', outcome: 'warned',
|
|
270
287
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
271
288
|
};
|
|
272
289
|
emitAudit(entry);
|
|
@@ -279,7 +296,8 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
279
296
|
log.warn(`[shieldcortex] ⚠️ requireApproval not available for ${severity} risk in ${context.toolName} — failure policy: ${failAction}`);
|
|
280
297
|
const entry = {
|
|
281
298
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
282
|
-
threats, anomalyScore,
|
|
299
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
300
|
+
action: 'require_approval',
|
|
283
301
|
outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
|
|
284
302
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
285
303
|
};
|
|
@@ -293,7 +311,8 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
293
311
|
log.warn('[shieldcortex] ⚠️ Too many approval prompts — auto-denying');
|
|
294
312
|
const entry = {
|
|
295
313
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
296
|
-
threats, anomalyScore,
|
|
314
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
315
|
+
action: 'rate_limit', outcome: 'auto_denied',
|
|
297
316
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
298
317
|
};
|
|
299
318
|
emitAudit(entry);
|
|
@@ -310,7 +329,8 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
310
329
|
log.warn(`[shieldcortex] ⚠️ requireApproval error: ${err instanceof Error ? err.message : err} — failure policy: ${failAction}`);
|
|
311
330
|
const entry = {
|
|
312
331
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
313
|
-
threats, anomalyScore,
|
|
332
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
333
|
+
action: 'require_approval',
|
|
314
334
|
outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
|
|
315
335
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
316
336
|
};
|
|
@@ -323,7 +343,8 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
323
343
|
if (approved) {
|
|
324
344
|
const entry = {
|
|
325
345
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
326
|
-
threats, anomalyScore,
|
|
346
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
347
|
+
action: 'require_approval', outcome: 'approved',
|
|
327
348
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
328
349
|
};
|
|
329
350
|
emitAudit(entry);
|
|
@@ -333,7 +354,8 @@ export function createInterceptor(config, pipeline, options) {
|
|
|
333
354
|
denyCache.addDenial(context.toolName, fullContent);
|
|
334
355
|
const entry = {
|
|
335
356
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
336
|
-
threats, anomalyScore,
|
|
357
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
358
|
+
action: 'require_approval', outcome: 'denied',
|
|
337
359
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
338
360
|
};
|
|
339
361
|
emitAudit(entry);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.32.
|
|
3
|
+
"version": "4.32.3",
|
|
4
4
|
"name": "ShieldCortex Real-time Scanner",
|
|
5
5
|
"description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
|
|
6
6
|
"kind": null,
|
package/index.ts
CHANGED
|
@@ -680,8 +680,9 @@ export async function scanLlmInput(event: LlmInputEvent, _ctx: AgentCtx): Promis
|
|
|
680
680
|
};
|
|
681
681
|
auditLog(entry);
|
|
682
682
|
loadConfig()
|
|
683
|
-
// Pass the local entry as-is; cloudSync
|
|
684
|
-
//
|
|
683
|
+
// Pass the local entry as-is; cloudSync rebuilds a canonical metadata-only
|
|
684
|
+
// entry from named fields and never reads preview/content. No raw LLM input
|
|
685
|
+
// leaves here.
|
|
685
686
|
.then(cfg => cloudSync(entry, cfg))
|
|
686
687
|
.catch(() => {});
|
|
687
688
|
}
|
package/intercept-ingest.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// plugins/openclaw/intercept-ingest.ts
|
|
2
|
+
import { toAuditEntry } from './audit-entry.js';
|
|
2
3
|
import type { InterceptAuditEntry } from './interceptor.js';
|
|
3
4
|
|
|
4
5
|
interface CloudConfig {
|
|
@@ -10,23 +11,36 @@ interface CloudConfig {
|
|
|
10
11
|
export function syncInterceptEvent(event: InterceptAuditEntry, config: CloudConfig): void {
|
|
11
12
|
if (!config.cloudEnabled || !config.cloudApiKey) return;
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
// Privacy: build a canonical audit entry from METADATA only — the content
|
|
15
|
+
// preview never leaves the machine (ClawScan finding: previews may contain
|
|
16
|
+
// credentials or confidential data). The local audit JSONL retains the
|
|
17
|
+
// preview for triage. toAuditEntry has no notion of preview/content.
|
|
18
|
+
const entry = toAuditEntry({
|
|
19
|
+
kind: 'intercept',
|
|
20
|
+
tool: event.tool,
|
|
21
|
+
firewallResult: event.firewallResult,
|
|
22
|
+
threats: event.threats,
|
|
23
|
+
anomalyScore: event.anomalyScore,
|
|
24
|
+
trustScore: event.trustScore,
|
|
25
|
+
sensitivityLevel: event.sensitivityLevel,
|
|
26
|
+
fragmentationScore: event.fragmentationScore,
|
|
27
|
+
outcome: event.outcome,
|
|
28
|
+
action: event.action,
|
|
29
|
+
pipelineDurationMs: event.pipelineDurationMs,
|
|
30
|
+
ts: event.ts,
|
|
31
|
+
});
|
|
32
|
+
if (!entry) return;
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
// SaaS /v1/audit/ingest requires { entries: [<canonical snake_case entry>] }
|
|
35
|
+
// (zod ingestSchema). The old { events: [...] } shape was rejected 400 and
|
|
36
|
+
// every interceptor POST was silently dropped.
|
|
37
|
+
fetch(`${config.cloudBaseUrl}/v1/audit/ingest`, {
|
|
22
38
|
method: 'POST',
|
|
23
39
|
headers: {
|
|
24
40
|
'Content-Type': 'application/json',
|
|
25
41
|
Authorization: `Bearer ${config.cloudApiKey}`,
|
|
26
42
|
},
|
|
27
|
-
body: JSON.stringify({
|
|
28
|
-
events: [{ ...metadata, source: 'openclaw-interceptor' }],
|
|
29
|
-
}),
|
|
43
|
+
body: JSON.stringify({ entries: [entry] }),
|
|
30
44
|
signal: AbortSignal.timeout(5_000),
|
|
31
45
|
}).catch(() => {
|
|
32
46
|
// Fire-and-forget — never block on cloud sync failure
|
package/interceptor.ts
CHANGED
|
@@ -27,6 +27,10 @@ export interface InterceptAuditEntry {
|
|
|
27
27
|
firewallResult: string;
|
|
28
28
|
threats: string[];
|
|
29
29
|
anomalyScore: number;
|
|
30
|
+
trustScore: number; // from the pipeline result's trust score
|
|
31
|
+
sensitivityLevel: string; // from the pipeline result's sensitivity level
|
|
32
|
+
fragmentationScore: number | null; // from the pipeline result's fragmentation score, or null
|
|
33
|
+
pipelineDurationMs: number; // wall-clock ms around the runDefencePipeline call
|
|
30
34
|
action: InterceptAction | 'auto_deny' | 'rate_limit';
|
|
31
35
|
outcome: 'approved' | 'denied' | 'auto_denied' | 'logged' | 'warned' | 'failure_allowed' | 'failure_denied';
|
|
32
36
|
preview: string;
|
|
@@ -271,6 +275,10 @@ function xrayMemoryGuard(content: string, title?: string): XRayGuardResult {
|
|
|
271
275
|
|
|
272
276
|
// --- Interceptor Factory ---
|
|
273
277
|
|
|
278
|
+
// Subset of shieldcortex/defence DefencePipelineResult (src/defence/pipeline.ts).
|
|
279
|
+
// Field paths verified against src/defence/types.ts: trust.score (TrustScore.score),
|
|
280
|
+
// sensitivity.level (SensitivityClassification.level), fragmentation.score
|
|
281
|
+
// (FragmentationAnalysis.score; fragmentation itself is nullable).
|
|
274
282
|
type PipelineRunner = (content: string, title: string, source: { type: string; identifier: string }) => {
|
|
275
283
|
allowed: boolean;
|
|
276
284
|
firewall: {
|
|
@@ -280,6 +288,9 @@ type PipelineRunner = (content: string, title: string, source: { type: string; i
|
|
|
280
288
|
anomalyScore: number;
|
|
281
289
|
blockedPatterns: string[];
|
|
282
290
|
};
|
|
291
|
+
trust: { score: number };
|
|
292
|
+
sensitivity: { level: string };
|
|
293
|
+
fragmentation: { score: number } | null;
|
|
283
294
|
auditId: number;
|
|
284
295
|
};
|
|
285
296
|
|
|
@@ -319,7 +330,10 @@ export function createInterceptor(
|
|
|
319
330
|
const xrayEntry: InterceptAuditEntry = {
|
|
320
331
|
type: 'intercept', tool: context.toolName, severity: 'critical',
|
|
321
332
|
firewallResult: 'BLOCK', threats: xrayResult.findings.map(f => f.category),
|
|
322
|
-
anomalyScore: 1,
|
|
333
|
+
anomalyScore: 1,
|
|
334
|
+
// X-Ray short-circuits before the pipeline runs — no pipeline result.
|
|
335
|
+
trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
|
|
336
|
+
action: 'auto_deny', outcome: 'auto_denied',
|
|
323
337
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
324
338
|
};
|
|
325
339
|
emitAudit(xrayEntry);
|
|
@@ -330,19 +344,30 @@ export function createInterceptor(
|
|
|
330
344
|
let firewallResult: string;
|
|
331
345
|
let threats: string[];
|
|
332
346
|
let anomalyScore: number;
|
|
347
|
+
let trustScore: number;
|
|
348
|
+
let sensitivityLevel: string;
|
|
349
|
+
let fragmentationScore: number | null;
|
|
350
|
+
let pipelineDurationMs: number;
|
|
333
351
|
|
|
334
352
|
try {
|
|
353
|
+
const pipelineStart = Date.now();
|
|
335
354
|
const result = pipeline(content, title, { type: 'agent', identifier: 'openclaw' });
|
|
355
|
+
pipelineDurationMs = Date.now() - pipelineStart;
|
|
336
356
|
severity = mapSeverity(result.firewall);
|
|
337
357
|
firewallResult = result.firewall.result;
|
|
338
358
|
threats = result.firewall.threatIndicators;
|
|
339
359
|
anomalyScore = result.firewall.anomalyScore;
|
|
360
|
+
trustScore = result.trust.score;
|
|
361
|
+
sensitivityLevel = result.sensitivity.level;
|
|
362
|
+
fragmentationScore = result.fragmentation?.score ?? null;
|
|
340
363
|
} catch (err) {
|
|
341
364
|
log.warn(`[shieldcortex] ⚠️ Defence pipeline error: ${err instanceof Error ? err.message : err}`);
|
|
342
365
|
const failAction = config.failurePolicy.high;
|
|
343
366
|
const entry: InterceptAuditEntry = {
|
|
344
367
|
type: 'intercept', tool: context.toolName, severity: 'high',
|
|
345
368
|
firewallResult: 'ERROR', threats: ['pipeline_error'], anomalyScore: 0,
|
|
369
|
+
// Pipeline threw — no result in scope, use documented defaults.
|
|
370
|
+
trustScore: 0, sensitivityLevel: 'INTERNAL', fragmentationScore: null, pipelineDurationMs: 0,
|
|
346
371
|
action: 'require_approval', outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
|
|
347
372
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
348
373
|
};
|
|
@@ -356,7 +381,8 @@ export function createInterceptor(
|
|
|
356
381
|
if (denyCache.isDenied(context.toolName, fullContent)) {
|
|
357
382
|
const entry: InterceptAuditEntry = {
|
|
358
383
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
359
|
-
threats, anomalyScore,
|
|
384
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
385
|
+
action: 'auto_deny', outcome: 'auto_denied',
|
|
360
386
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
361
387
|
};
|
|
362
388
|
emitAudit(entry);
|
|
@@ -368,7 +394,8 @@ export function createInterceptor(
|
|
|
368
394
|
if (action === 'log') {
|
|
369
395
|
const entry: InterceptAuditEntry = {
|
|
370
396
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
371
|
-
threats, anomalyScore,
|
|
397
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
398
|
+
action: 'log', outcome: 'logged',
|
|
372
399
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
373
400
|
};
|
|
374
401
|
emitAudit(entry);
|
|
@@ -379,7 +406,8 @@ export function createInterceptor(
|
|
|
379
406
|
log.warn(`[shieldcortex] ⚠️ ${severity} risk in ${context.toolName}: ${threats.join(', ') || 'anomaly detected'}`);
|
|
380
407
|
const entry: InterceptAuditEntry = {
|
|
381
408
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
382
|
-
threats, anomalyScore,
|
|
409
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
410
|
+
action: 'warn', outcome: 'warned',
|
|
383
411
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
384
412
|
};
|
|
385
413
|
emitAudit(entry);
|
|
@@ -393,7 +421,8 @@ export function createInterceptor(
|
|
|
393
421
|
log.warn(`[shieldcortex] ⚠️ requireApproval not available for ${severity} risk in ${context.toolName} — failure policy: ${failAction}`);
|
|
394
422
|
const entry: InterceptAuditEntry = {
|
|
395
423
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
396
|
-
threats, anomalyScore,
|
|
424
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
425
|
+
action: 'require_approval',
|
|
397
426
|
outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
|
|
398
427
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
399
428
|
};
|
|
@@ -408,7 +437,8 @@ export function createInterceptor(
|
|
|
408
437
|
log.warn('[shieldcortex] ⚠️ Too many approval prompts — auto-denying');
|
|
409
438
|
const entry: InterceptAuditEntry = {
|
|
410
439
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
411
|
-
threats, anomalyScore,
|
|
440
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
441
|
+
action: 'rate_limit', outcome: 'auto_denied',
|
|
412
442
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
413
443
|
};
|
|
414
444
|
emitAudit(entry);
|
|
@@ -426,7 +456,8 @@ export function createInterceptor(
|
|
|
426
456
|
log.warn(`[shieldcortex] ⚠️ requireApproval error: ${err instanceof Error ? err.message : err} — failure policy: ${failAction}`);
|
|
427
457
|
const entry: InterceptAuditEntry = {
|
|
428
458
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
429
|
-
threats, anomalyScore,
|
|
459
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
460
|
+
action: 'require_approval',
|
|
430
461
|
outcome: failAction === 'deny' ? 'failure_denied' : 'failure_allowed',
|
|
431
462
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
432
463
|
};
|
|
@@ -440,7 +471,8 @@ export function createInterceptor(
|
|
|
440
471
|
if (approved) {
|
|
441
472
|
const entry: InterceptAuditEntry = {
|
|
442
473
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
443
|
-
threats, anomalyScore,
|
|
474
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
475
|
+
action: 'require_approval', outcome: 'approved',
|
|
444
476
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
445
477
|
};
|
|
446
478
|
emitAudit(entry);
|
|
@@ -451,7 +483,8 @@ export function createInterceptor(
|
|
|
451
483
|
denyCache.addDenial(context.toolName, fullContent);
|
|
452
484
|
const entry: InterceptAuditEntry = {
|
|
453
485
|
type: 'intercept', tool: context.toolName, severity, firewallResult,
|
|
454
|
-
threats, anomalyScore,
|
|
486
|
+
threats, anomalyScore, trustScore, sensitivityLevel, fragmentationScore, pipelineDurationMs,
|
|
487
|
+
action: 'require_approval', outcome: 'denied',
|
|
455
488
|
preview: fullContent.slice(0, 200), ts: new Date().toISOString(),
|
|
456
489
|
};
|
|
457
490
|
emitAudit(entry);
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "shieldcortex-realtime",
|
|
3
|
-
"version": "4.32.
|
|
3
|
+
"version": "4.32.3",
|
|
4
4
|
"name": "ShieldCortex Real-time Scanner",
|
|
5
5
|
"description": "Real-time defence scanning on LLM input, memory extraction on LLM output, and active tool call interception with approval gating.",
|
|
6
6
|
"kind": null,
|
package/package.json
CHANGED