commit a8d061235f7850366aff9b38f841d872105ac39f
parent 63c14b53caf82c72378d2974846127213068ba55
Author: Randy Concepcion <rconcepcion@mozilla.com>
Date: Tue, 23 Dec 2025 21:05:28 +0000
Bug 2003214 - Add security audit logger to AI Window security layer r=ai-ondevice-reviewers,gregtatum
Implements console-based security audit logging for policy decisions:
- SecurityLogger.sys.mjs: Logs to Browser Console
- Summary line for quick visibility (ALLOW/DENY with timing)
- Full event object for detailed debugging (expandable in Browser Console)
- Controlled via browser.ml.logLevel pref
Differential Revision: https://phabricator.services.mozilla.com/D274680
Diffstat:
4 files changed, 207 insertions(+), 18 deletions(-)
diff --git a/toolkit/components/ml/security/SecurityLogger.sys.mjs b/toolkit/components/ml/security/SecurityLogger.sys.mjs
@@ -2,6 +2,31 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+/**
+ * Security audit logger for AI Window policy decisions.
+ * Outputs logs for debugging and development.
+ *
+ * ## Viewing Logs
+ *
+ * Logs appear in the Browser Console (Ctrl+Shift+J) and terminal.
+ *
+ * To enable debug-level output:
+ * ./mach run --setpref browser.ml.logLevel=Debug
+ *
+ * Or using MOZ_LOG:
+ * MOZ_LOG=SecurityLogger:5 ./mach run
+ *
+ * To filter in Browser Console:
+ * Type "SecurityLogger" in the filter box
+ *
+ * To save logs from Browser Console:
+ * Right-click --> "Save all Messages to File"
+ * Then filter: grep "SecurityLogger" security.log
+ *
+ * To capture terminal output:
+ * ./mach run 2>&1 | grep "SecurityLogger" | tee security.log
+ */
+
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = XPCOMUtils.declareLazy({
@@ -14,25 +39,22 @@ const lazy = XPCOMUtils.declareLazy({
});
/**
- * Security audit logger (stub - console only).
- *
- * TODO (Bug 2003214): Phase 2 POC - Part 2: Implement Security Logger
- */
-
-/**
* Logs a security decision event.
*
* @param {object} event - The security event to log
+ * @param {string} event.requestId - Request identifier
+ * @param {string} event.sessionId - Session identifier
* @param {string} event.phase - Security phase (tool.execution, etc.)
- * @param {object} event.action - Action that was checked
- * @param {object} event.context - Request context
- * @param {object} event.decision - Policy decision (allow/deny)
+ * @param {object} event.action - Action details (type, tool, urls, args)
+ * @param {object} event.context - Context summary (tainted, trustedCount)
+ * @param {object} event.decision - Policy decision (effect, policyId, code, reason)
* @param {number} event.durationMs - Evaluation duration in milliseconds
* @param {Error} [event.error] - Optional error if evaluation failed
*/
export function logSecurityEvent(event) {
const { phase, decision, durationMs, error } = event;
+ // Summary line for quick visibility
if (error) {
lazy.console.error(
`[${phase}] Security evaluation error:`,
@@ -45,4 +67,7 @@ export function logSecurityEvent(event) {
} else {
lazy.console.debug(`[${phase}] ALLOW (${durationMs}ms)`);
}
+
+ // Full event for detailed debugging (object for Browser Console interactivity)
+ lazy.console.debug(`[${phase}] Event:`, event);
}
diff --git a/toolkit/components/ml/security/SecurityOrchestrator.sys.mjs b/toolkit/components/ml/security/SecurityOrchestrator.sys.mjs
@@ -239,9 +239,14 @@ export class SecurityOrchestrator {
if (!isSecurityEnabled()) {
lazy.logSecurityEvent({
+ requestId: context.requestId,
+ sessionId: this.#sessionId,
phase,
action,
- context,
+ context: {
+ tainted: context.tainted ?? false,
+ trustedCount: 0,
+ },
decision: {
effect: lazy.EFFECT_ALLOW,
reason: "Security disabled via preference flag",
@@ -254,10 +259,22 @@ export class SecurityOrchestrator {
const policies = this.#policies.get(phase);
if (!policies || policies.length === 0) {
- lazy.console.warn(
- `[Security] No policies registered for phase: ${phase}`
- );
- return lazy.createAllowDecision({ note: "No policies for phase" });
+ const decision = lazy.createAllowDecision({
+ reason: "No policies for phase",
+ });
+ lazy.logSecurityEvent({
+ requestId: context.requestId,
+ sessionId: this.#sessionId,
+ phase,
+ action,
+ context: {
+ tainted: context.tainted ?? false,
+ trustedCount: 0,
+ },
+ decision,
+ durationMs: ChromeUtils.now() - startTime,
+ });
+ return decision;
}
const fullContext = {
@@ -279,9 +296,14 @@ export class SecurityOrchestrator {
);
lazy.logSecurityEvent({
+ requestId: context.requestId,
+ sessionId: this.#sessionId,
phase,
action,
- context: fullContext,
+ context: {
+ tainted: context.tainted ?? false,
+ trustedCount: linkLedger?.size() ?? 0,
+ },
decision,
durationMs: ChromeUtils.now() - startTime,
});
@@ -295,9 +317,14 @@ export class SecurityOrchestrator {
);
lazy.logSecurityEvent({
- phase: envelope.phase || "unknown",
- action: envelope.action || {},
- context: envelope.context || {},
+ requestId: envelope?.context?.requestId,
+ sessionId: this.#sessionId,
+ phase: envelope?.phase || "unknown",
+ action: envelope?.action || {},
+ context: {
+ tainted: envelope?.context?.tainted ?? false,
+ trustedCount: 0,
+ },
decision: errorDecision,
durationMs: ChromeUtils.now() - startTime,
error,
diff --git a/toolkit/components/ml/tests/xpcshell/test_security_logger.js b/toolkit/components/ml/tests/xpcshell/test_security_logger.js
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Unit tests for SecurityLogger.sys.mjs
+ *
+ * Tests verify that logging operations complete without error.
+ * The logger outputs to Browser Console for developer debugging.
+ */
+
+const { logSecurityEvent } = ChromeUtils.importESModule(
+ "chrome://global/content/ml/security/SecurityLogger.sys.mjs"
+);
+
+function createTestEntry(overrides = {}) {
+ return {
+ requestId: "req-123",
+ sessionId: "session-456",
+ phase: "tool.execution",
+ action: {
+ type: "tool.call",
+ tool: "get_page_content",
+ urls: ["https://example.com/page"],
+ args: { mode: "full" },
+ },
+ context: {
+ tainted: true,
+ trustedCount: 5,
+ },
+ decision: {
+ effect: "allow",
+ },
+ durationMs: 1.5,
+ ...overrides,
+ };
+}
+
+/**
+ * Test: logSecurityEvent completes without error.
+ *
+ * Reason:
+ * Basic smoke test—logging should never throw. Failures in logging
+ * shouldn't break security evaluation flow.
+ */
+add_task(function test_logSecurityEvent_completes_without_error() {
+ logSecurityEvent(createTestEntry());
+ Assert.ok(true, "logSecurityEvent() completed without error");
+});
+
+/**
+ * Test: logSecurityEvent handles deny decisions.
+ *
+ * Reason:
+ * Deny decisions include extra fields (policyId, code, reason).
+ * Logger must handle the full deny structure without error.
+ */
+add_task(function test_logSecurityEvent_handles_deny_decision() {
+ logSecurityEvent(
+ createTestEntry({
+ decision: {
+ effect: "deny",
+ policyId: "block-unseen-links",
+ code: "UNSEEN_LINK",
+ reason: "URL not in trusted context",
+ },
+ })
+ );
+ Assert.ok(true, "logSecurityEvent() handles deny decision without error");
+});
+
+/**
+ * Test: logSecurityEvent handles missing optional fields.
+ *
+ * Reason:
+ * Not all actions have URLs or args. Logger must gracefully handle
+ * minimal entries without throwing on missing optional fields.
+ */
+add_task(function test_logSecurityEvent_handles_missing_optional_fields() {
+ logSecurityEvent({
+ requestId: "req-minimal",
+ sessionId: "session-minimal",
+ phase: "tool.execution",
+ action: {
+ type: "tool.call",
+ tool: "search_tabs",
+ },
+ context: {},
+ decision: { effect: "allow" },
+ durationMs: 0.5,
+ });
+ Assert.ok(
+ true,
+ "logSecurityEvent() handles missing optional fields without error"
+ );
+});
+
+/**
+ * Test: logSecurityEvent handles error entries.
+ *
+ * Reason:
+ * When evaluation fails with an exception, the error is logged.
+ * Logger must handle Error objects without throwing.
+ */
+add_task(function test_logSecurityEvent_handles_error_entry() {
+ logSecurityEvent(
+ createTestEntry({
+ error: new Error("Test error"),
+ })
+ );
+ Assert.ok(true, "logSecurityEvent() handles error entries without error");
+});
+
+/**
+ * Test: logSecurityEvent handles multiple URLs.
+ *
+ * Reason:
+ * Tool calls may request multiple URLs. Logger must handle
+ * multiple URLs without error.
+ */
+add_task(function test_logSecurityEvent_handles_multiple_urls() {
+ logSecurityEvent(
+ createTestEntry({
+ action: {
+ type: "tool.call",
+ tool: "get_page_content",
+ urls: [
+ "https://example.com/page1",
+ "https://example.com/page2",
+ "https://example.com/page3",
+ ],
+ },
+ })
+ );
+ Assert.ok(true, "logSecurityEvent() handles multiple URLs without error");
+});
diff --git a/toolkit/components/ml/tests/xpcshell/xpcshell.toml b/toolkit/components/ml/tests/xpcshell/xpcshell.toml
@@ -11,5 +11,6 @@ prefs = [
["test_json_policy_system.js"]
["test_ml_engine_security.js"]
["test_policy_evaluator.js"]
+["test_security_logger.js"]
["test_security_orchestrator.js"]
["test_security_utils.js"]