tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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:
Mtoolkit/components/ml/security/SecurityLogger.sys.mjs | 43++++++++++++++++++++++++++++++++++---------
Mtoolkit/components/ml/security/SecurityOrchestrator.sys.mjs | 45++++++++++++++++++++++++++++++++++++---------
Atoolkit/components/ml/tests/xpcshell/test_security_logger.js | 136+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/ml/tests/xpcshell/xpcshell.toml | 1+
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"]