Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions browser/components/enterprisepolicies/Policies.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ ChromeUtils.defineESModuleGetters(lazy, {
AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
AddonRepository: "resource://gre/modules/addons/AddonRepository.sys.mjs",
BookmarksPolicies: "resource:///modules/policies/BookmarksPolicies.sys.mjs",
BuiltinDataLossPrevention:
"resource:///modules/policies/BuiltinDataLossPrevention.sys.mjs",
CustomizableUI:
"moz-src:///browser/components/customizableui/CustomizableUI.sys.mjs",
ExtensionPermissions: "resource://gre/modules/ExtensionPermissions.sys.mjs",
Expand Down Expand Up @@ -724,6 +726,19 @@ export var Policies = {
},
},

BuiltinDataLossPrevention: {
onBeforeAddons(manager, param) {
if (param.Enabled) {
lazy.BuiltinDataLossPrevention.enable(manager, param);
} else {
lazy.BuiltinDataLossPrevention.disable(manager);
}
},
onRemove(manager, _oldParams) {
lazy.BuiltinDataLossPrevention.disable(manager);
},
},

CaptivePortal: {
onBeforeAddons(manager, param) {
setAndLockPref("network.captive-portal-service.enabled", param);
Expand Down Expand Up @@ -1004,6 +1019,10 @@ export var Policies = {
// to be consistent.
Services.prefs.lockPref("browser.contentanalysis.enabled");
}
lazy.BuiltinDataLossPrevention.updateDLPMode(manager);
},
onRemove(manager, _oldParams) {
lazy.BuiltinDataLossPrevention.updateDLPMode(manager);
},
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ function generateErrors() {
"WindowsGPOParser",
"Enterprise Policies Child",
"BookmarksPolicies",
"BuiltinDataLossPrevention",
"ProxyPolicies",
"WebsiteFilter Policy",
"macOSPoliciesParser",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/* 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/. */

import {
setAndLockPref,
unsetAndUnlockPref,
} from "resource:///modules/policies/Policies.sys.mjs";

const PREF_LOGLEVEL = "browser.policies.loglevel";
const DLP_MODE_PREF = "enterprise.dlp.mode";
const BUILTIN_RULES_PREF = "enterprise.dlp.builtin.rules";

const VALID_ACTIONS = [
"ClipboardCopy",
"ClipboardPaste",
"Download",
"DragAndDrop",
"FileUpload",
"Print",
];
const VALID_TYPES = ["warn", "block"];
const NAME_REGEX = /^[a-z0-9-]{1,64}$/;

const lazy = {};

ChromeUtils.defineLazyGetter(lazy, "log", () => {
let { ConsoleAPI } = ChromeUtils.importESModule(
"resource://gre/modules/Console.sys.mjs"
);
return new ConsoleAPI({
prefix: "BuiltinDataLossPrevention",
maxLogLevel: "error",
maxLogLevelPref: PREF_LOGLEVEL,
});
});

/**
* Helper for the BuiltinDataLossPrevention enterprise policy.
*
* The built-in DLP and ContentAnalysis policies share a single derived
* `enterprise.dlp.mode` pref. ContentAnalysis takes precedence: when both
* policies are enabled the mode is "contentanalysis"; when only the built-in
* policy is enabled it is "builtin"; when neither is enabled the pref is
* cleared. Both policies call `updateDLPMode` whenever they are applied or
* removed so the mode always reflects the final active policy set.
*/
export const BuiltinDataLossPrevention = {
/**
* Recompute the shared `enterprise.dlp.mode` pref from the active policy set.
*
* Reads the final active policies (so application order is irrelevant) and
* resolves precedence in one place. Called by both the BuiltinDataLossPrevention
* and ContentAnalysis handlers on add and remove.
*
* @param {object} manager - The enterprise policy manager, used to query the
* currently active policies via `getActivePolicies()`.
*/
updateDLPMode(manager) {
const activePolicies = manager.getActivePolicies();
const caActive = activePolicies?.ContentAnalysis?.Enabled === true;
const builtinActive =
activePolicies?.BuiltinDataLossPrevention?.Enabled === true;

if (!caActive && !builtinActive) {
unsetAndUnlockPref(DLP_MODE_PREF);
return;
}

setAndLockPref(DLP_MODE_PREF, caActive ? "contentanalysis" : "builtin");
},

/**
* Validate the configured rules, dropping any that are invalid.
*
* Each rule is checked independently; an invalid rule is skipped and logged
* to about:policies#errors while the remaining rules still load. Enforces:
* - Name unique within the policy and matching [a-z0-9-]{1,64}
* - Actions non-empty and drawn only from the known interception points
* - Domains present and non-empty (["*"] matches all domains)
* - Type (when present) is "warn" or "block"
*
* @param {Array<object>} rules - The raw Rules array from the policy.
* @returns {Array<object>} The subset of rules that passed validation.
*/
validateRules(rules) {
const valid = [];
const seenNames = new Set();

for (const rule of rules) {
const name = rule.Name;

if (typeof name !== "string" || !NAME_REGEX.test(name)) {
lazy.log.error(
`BuiltinDataLossPrevention: skipping rule with invalid Name ` +
`${JSON.stringify(name)} - must match [a-z0-9-]{1,64}.`
);
continue;
}

if (seenNames.has(name)) {
lazy.log.error(
`BuiltinDataLossPrevention: skipping rule "${name}" - Name must be ` +
`unique within the policy.`
);
continue;
}

if (
!Array.isArray(rule.Actions) ||
!rule.Actions.length ||
!rule.Actions.every(action => VALID_ACTIONS.includes(action))
) {
lazy.log.error(
`BuiltinDataLossPrevention: skipping rule "${name}" - Actions must ` +
`be non-empty and only contain ${VALID_ACTIONS.join(", ")}.`
);
continue;
}

if (
!Array.isArray(rule.Domains) ||
!rule.Domains.length ||
!rule.Domains.every(
domain => typeof domain === "string" && domain.length
)
) {
lazy.log.error(
`BuiltinDataLossPrevention: skipping rule "${name}" - Domains must ` +
`be present and contain only non-empty strings (use ["*"] for all ` +
`domains).`
);
continue;
}

if ("Type" in rule && !VALID_TYPES.includes(rule.Type)) {
lazy.log.error(
`BuiltinDataLossPrevention: skipping rule "${name}" - Type must be ` +
`"warn" or "block".`
);
continue;
}

seenNames.add(name);
valid.push(rule);
}

return valid;
},

/**
* Enable built-in DLP: validate and persist the rules, then recompute the
* DLP mode.
*
* Valid rules (including those with `Enabled: false`, which load but stay
* inactive) are written to the locked `enterprise.dlp.builtin.rules` pref as
* a JSON string for the DLP engine to consume.
*
* @param {object} manager - The enterprise policy manager.
* @param {object} param - The BuiltinDataLossPrevention policy object.
* @param {Array<object>} [param.Rules] - The configured DLP rules.
*/
enable(manager, param) {
const rules = this.validateRules(param.Rules ?? []);
setAndLockPref(BUILTIN_RULES_PREF, JSON.stringify(rules));
this.updateDLPMode(manager);
},

/**
* Disable built-in DLP: clear the persisted rules and recompute the DLP mode.
*
* @param {object} manager - The enterprise policy manager.
*/
disable(manager) {
unsetAndUnlockPref(BUILTIN_RULES_PREF);
this.updateDLPMode(manager);
},
};
1 change: 1 addition & 0 deletions browser/components/enterprisepolicies/helpers/moz.build
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ with Files("**"):
EXTRA_JS_MODULES.policies += [
"AIChatbotPolicies.sys.mjs",
"BookmarksPolicies.sys.mjs",
"BuiltinDataLossPrevention.sys.mjs",
"ProxyPolicies.sys.mjs",
"WebsiteFilter.sys.mjs",
]
Expand Down
47 changes: 47 additions & 0 deletions browser/components/enterprisepolicies/schemas/policies-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,53 @@
}
}
},
"BuiltinDataLossPrevention": {
"type": "object",
"properties": {
"Enabled": {
"type": "boolean"
},
"Rules": {
"type": "array",
"strict": false,
"items": {
"type": "object",
"properties": {
"Name": {
"type": "string"
},
"Enabled": {
"type": "boolean"
},
"Actions": {
"type": "array",
"items": {
"type": "string"
}
},
"Domains": {
"type": "array",
"items": {
"type": "string"
}
},
"Posture": {
"type": "string"
},
"Type": {
"type": "string"
},
"Message": {
"type": "string"
}
},
"required": ["Name", "Actions", "Domains"]
}
}
},
"x-category": "Security",
"description": "Enable Firefox's built-in Data Loss Prevention. If ContentAnalysis is also enabled, it takes precedence and the built-in DLP will not activate."
},
"CaptivePortal": {
"type": "boolean",
"x-category": "Network security",
Expand Down
Loading