File size: 15,011 Bytes
5a60197 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | """One plain sentence per draft saying what the body does, plus the policy name.
Many admins read only this sentence, so it describes the body, not the request.
"""
from __future__ import annotations
from .model import Draft
from .platform import catalog
from .platform.context import PlatformContext
ACTION_PHRASE = {'block': 'Block', 'warn': 'Warn about', 'redact': 'Redact', 'log': 'Log',
'alert': 'Alert on', 'remediate': 'Recall', 'delete': 'Delete'}
def _rules(d: Draft, ctx: PlatformContext, ids: list[str]) -> str:
names = []
for rid in ids:
if rid == '$NEW_RULE' and d.ruleDecision.new_rule:
names.append(f'new rule "{d.ruleDecision.new_rule["name"]}"')
elif ctx.rule(rid):
names.append(f'"{ctx.rule(rid).name}"')
return ', '.join(names) if names else 'no rule'
def _what(d: Draft, ctx: PlatformContext) -> str:
"""A short name for the data a draft looks for: "Aadhaar, PAN" or "Payment Card Data"."""
rd = d.ruleDecision
if rd.mode == 'superset' and rd.rule_ids and ctx.rule(rd.rule_ids[-1]):
asked = [e for e in ctx.rule(rd.rule_ids[-1]).entities if e not in rd.extras]
return _list([_short(e) for e in asked], 3)
if rd.new_rule:
return rd.new_rule['name'].replace('Copilot - ', '')
names = [ctx.rule(r).name for r in rd.rule_ids if ctx.rule(r)]
if len(names) > 2:
return 'sensitive data' # a name, not an inventory: the summary lists the rules
return _list(names, 2) if names else 'data'
def _short(entity: str) -> str:
"""'Aadhaar Number' -> 'Aadhaar', 'PAN Card' -> 'PAN', 'Credit Card Number' -> 'Credit Card'."""
label = catalog.entity_label(entity)
return {'PAN Card': 'PAN', 'Card CVV': 'CVV', 'US SSN': 'SSN'}.get(label, label.replace(' Number', ''))
def _list(items: list[str], n: int = 4) -> str:
items = list(items)
if len(items) <= n:
return ', '.join(items)
return ', '.join(items[:n]) + f' and {len(items) - n} more'
def summarize(d: Draft, ctx: PlatformContext) -> tuple[str, str]:
"""(summary sentence, policy name)."""
b = d.policyBody
t = d.policyType
parts: list[str] = []
name_bits: list[str] = []
if t == 'endpoint':
ac, ext = b['agentControl'], b['extensionControl']
hosts = b['enabledSites']
names = {s['host']: s['name'] for s in catalog.ai_sites()}
where = 'all AI sites' if set(hosts) == set(catalog.ai_hosts()) else _list([names.get(h, h) for h in hosts])
if ext['detectionRuleIds']:
parts.append(f'{ACTION_PHRASE[b["action"]]} AI prompts on {where} that match '
f'{_rules(d, ctx, ext["detectionRuleIds"])}')
short_where = 'AI' if where == 'all AI sites' else where
name_bits.append(f'{ACTION_PHRASE[b["action"]].split()[0]} {_what(d, ctx)} in {short_where}')
elif any(i.code == 'NO_DETECTION_RULES' for i in d.issues):
parts.append(f'{ACTION_PHRASE[b["action"]]} AI prompts on {where} (choose the data to look for)')
name_bits.append('AI prompts')
if not b['allowFileUpload']:
parts.append(f'block file uploads to {where}')
name_bits.append('No AI uploads')
if not b['allowCodePaste']:
parts.append(f'block pasting code into {where}')
name_bits.append('No code paste to AI')
switches = {
'blockRemovableStorage': 'block USB storage', 'removableStorageReadOnly': 'make USB storage read-only',
'blockPrint': 'block printing', 'blockBluetooth': 'block Bluetooth file transfer',
'blockMobileSync': 'block phone sync', 'blockScreenCapture': 'block screenshots',
'blockCloudUpload': 'block cloud storage uploads', 'blockPersonalEmail': 'block personal webmail',
'blockFileSharing': 'block file-sharing sites', 'blockWebUpload': 'block web and FTP uploads',
'blockDebugTools': 'block debug and proxy tools', 'watermarkPrints': 'watermark printed pages',
'captureScreenshotOnViolation': 'take a screenshot when a policy is broken',
'dlpScanBeforeTransfer': f'scan files before USB or cloud transfer (level {ac["sensitivityThreshold"]})',
}
for k, phrase in switches.items():
if ac.get(k):
if k == 'blockCloudUpload':
svc = [s for s, f in (('Google Drive', 'blockGoogleDrive'), ('OneDrive', 'blockOneDrive'),
('SharePoint', 'blockSharePoint'), ('Dropbox', 'blockDropbox'),
('Box', 'blockBox'), ('iCloud', 'blockiCloud')) if ac.get(f)]
phrase = f'block uploads to {_list(svc)}' if svc else phrase
if k == 'blockPersonalEmail':
svc = [s for s, f in (('Gmail', 'blockGmail'), ('Outlook.com', 'blockOutlookPersonal'),
('Yahoo Mail', 'blockYahooMail'), ('Proton Mail', 'blockProtonMail'))
if ac.get(f)]
phrase = f'block {_list(svc)}' if svc else phrase
parts.append(phrase)
name_bits.append(phrase[0].upper() + phrase[1:])
if ac['allowedUsbDeviceIds']:
parts.append(f'except USB devices {_list(ac["allowedUsbDeviceIds"])}')
if ac['clipboardMode'] != 'off':
mode = {'audit': 'log copy-paste of personal data',
'everywhere': 'clear the clipboard when it holds personal data',
'targets': f'block pasting personal data into {_list(ac["pasteBlockedSites"] + ac["pasteBlockedApps"])}'
}[ac['clipboardMode']]
parts.append(mode)
name_bits.append('Clipboard protection')
for key, kind in (('webBlockListIds', 'websites'), ('appBlockListIds', 'apps')):
ids = ac[key]
if ids:
names_ = []
for lid in ids:
nl = next((x for x in d.newBlockLists if x['placeholder'] == lid), None)
names_.append(f'"{nl["body"]["name"]}"' if nl else f'"{ctx.block_list(lid).name}"')
parts.append(f'block {kind} on {_list(names_)}')
name_bits.append(f'Block {_list([n.strip(chr(34)).replace("Copilot - Blocked sites: ", "").replace("Copilot - Blocked apps: ", "") for n in names_], 2)}')
if b['activeWebCategories']:
cats = b['activeWebCategories']
parts.append(f'block websites in the {_list(cats)} categor{"y" if len(cats) == 1 else "ies"}')
name_bits.append(f'Block {_list(cats, 2)} sites')
ext_switches = {'enforceIncognito': 'block incognito windows', 'blockGuestMode': 'block guest mode',
'blockOtherBrowsers': 'allow only Chrome, Edge and Brave',
'blockDeveloperMode': 'block browser developer mode', 'blockDevTools': 'block browser developer tools',
'blockDownloads': 'block browser downloads',
'enforceExtensionAllowlist': 'allow only approved browser extensions'}
for k, phrase in ext_switches.items():
if ext.get(k) and ext.get(k) != catalog.default_body('endpoint')['extensionControl'].get(k):
parts.append(phrase)
name_bits.append(phrase[0].upper() + phrase[1:])
if not ext['monitorDownloads']:
parts.append('stop logging downloads')
sr = ac['signInRestriction']
if sr['enabled']:
parts.append(f'allow sign-in only with {_list(sr["allowedDomains"])} accounts')
if sr['strictLogin']:
parts.append('refuse sign-in on websites that are not approved')
name_bits.append('Sign-in restriction')
ide = ac['ideControl']
if ide.get('enabled'):
bits = []
if ide.get('detectionRuleIds'):
bits.append(f'{ide["promptAction"]} prompts to AI coding tools that match '
f'{_rules(d, ctx, ide["detectionRuleIds"])}')
if ide.get('blockedAiTools'):
tools = {t['key']: t['name'] for t in catalog.ide()['tools']}
bits.append(f'block {_list([tools.get(x, x) for x in ide["blockedAiTools"]])}')
if ide.get('providerControl') == 'allowlist':
provs = {p['id']: p['name'] for p in catalog.ide()['providers']}
bits.append(f'allow only {_list([provs.get(x, x) for x in ide.get("allowedProviders", [])], 5)} '
f'in AI coding tools')
if ide.get('mcpControl') == 'denylist':
bits.append('block the named MCP servers')
if ide.get('commandAction') != 'warn':
bits.append(f'{ide["commandAction"]} shell commands run by AI agents')
if ide.get('contextAction') != 'warn':
bits.append(f'{ide["contextAction"]} AI agents reading sensitive files')
if ide.get('enforce') is False:
bits.append('monitor AI coding tools without enforcing')
if ide.get('storePromptContent'):
bits.append('store the text of prompts to AI coding tools')
if ide.get('interceptPrompts'):
bits.append('intercept prompts from AI command-line tools')
if ide.get('inspectResponses'):
bits.append('inspect AI responses')
if ide.get('claudeCodeHook'):
bits.append('use the Claude Code hook')
if ide.get('fileAccessAction', 'off') != 'off':
verb_ = {'close': 'close', 'warn': 'warn about', 'log': 'log'}[ide['fileAccessAction']]
bits.append(f'{verb_} AI tools that open sensitive files ({ide["fileAccessThreshold"].lower()} risk and up)')
if ide.get('inEditorAlerts') is False:
bits.append('turn off in-editor alerts')
if ide.get('discoverAiTools') is False:
bits.append('stop discovering AI tools')
parts += bits or ['turn on AI coding tool monitoring']
name_bits.append('AI coding tools' + (': allow only ' + _list(
[p for p in ide.get('allowedProviders', [])], 3) if ide.get('providerControl') == 'allowlist' else ''))
if b['testMode']:
parts.append('in test mode (nothing is enforced)')
grp = ctx.group(b['groupId']).name if b['groupId'] != 'default' and ctx.group(b['groupId']) else None
scope = f'for {grp}' if grp else 'for everyone'
else:
cfg = b['channels'][t]
label = catalog.TYPE_LABEL[t]
rules = cfg.get('detectionRuleIds') or cfg.get('sharingDetectionRuleIds') or []
if t == 'email':
act = ACTION_PHRASE[cfg['action']]
if rules:
parts.append(f'{act} emails that match {_rules(d, ctx, rules)}')
if cfg.get('allowedDomains'):
parts.append(f'except mail only to {_list(cfg["allowedDomains"])}')
if cfg.get('blockedDomains'):
parts.append(f'alert on every email to {_list(cfg["blockedDomains"])}')
if cfg.get('checkBulkForwarding'):
parts.append(f'flag anyone sending {cfg["bulkForwardThreshold"]}+ emails an hour')
if cfg.get('scheduleRestrict'):
parts.append(f'treat sensitive mail outside {cfg["allowedHoursStart"]}:00-{cfg["allowedHoursEnd"]}:00 UTC '
f'as high risk')
name_bits.append(f'{act.split()[0]} {_what(d, ctx)} in email' if rules else 'Email monitoring')
elif t == 'slack':
act = 'Delete' if cfg['action'] == 'delete' else 'Log'
parts.append(f'{act} Slack messages that match {_rules(d, ctx, rules)}')
name_bits.append(f'{act} {_what(d, ctx)} in Slack')
elif t in ('teams', 'chat'):
parts.append(f'Record {label} messages that match {_rules(d, ctx, rules)}')
name_bits.append(f'Monitor {_what(d, ctx)} in {label}')
elif t in ('drive', 'm365files'):
if rules:
parts.append(f'Record {label} shares of files that match {_rules(d, ctx, rules)}')
else:
parts.append(f'Record risky {label} shares')
if cfg.get('autoRevokePublic'):
parts.append('remove public links')
if cfg.get('autoRevokeExternal'):
parts.append('remove external shares')
if cfg.get('trustedDomains'):
parts.append(f'except shares with {_list(cfg["trustedDomains"])}')
if cfg.get('excludedUsers'):
parts.append(f'except {_list(cfg["excludedUsers"])}')
dspm = cfg.get('dspm') or {}
if dspm.get('scanMode') == 'rules' and dspm.get('detectionRuleIds'):
parts.append(f'scan stored files for {_rules(d, ctx, dspm["detectionRuleIds"])}')
undo = ' + '.join(x for x, on in (('remove public links', cfg.get('autoRevokePublic')),
('remove external shares', cfg.get('autoRevokeExternal'))) if on)
name_bits.append(f'{label}: {_what(d, ctx) if rules else "risky shares"}' + (f' ({undo})' if undo else ''))
else:
if cfg.get('scanMode') == 'rules':
parts.append(f'Scan {label} for {_rules(d, ctx, cfg["detectionRuleIds"])}')
else:
parts.append(f'Scan {label} for any sensitive data (discovery)')
if cfg.get('bulkRepoThreshold') and t == 'github':
parts.append(f'flag anyone cloning {cfg["bulkRepoThreshold"]}+ repositories an hour')
if (cfg.get('autoSync') or {}).get('enabled'):
parts.append(f're-scan changes every {cfg["autoSync"]["intervalMinutes"]} minutes')
name_bits.append(f'Scan {label} for {_what(d, ctx)}' if cfg.get('scanMode') == 'rules'
else f'Scan {label} (discovery)')
if t != 'email' and cfg.get('connectorIds'):
names_ = [c.name for c in ctx.connectors_for(t) if c.id in cfg['connectorIds']]
parts.append(f'in {_list(names_)}')
scope = 'for the whole organisation'
sentence = '; '.join(parts) if parts else 'No settings changed'
sentence = sentence[0].upper() + sentence[1:] + f', {scope}. Saved switched off.'
name = ' + '.join(dict.fromkeys(name_bits)) or catalog.TYPE_LABEL[t]
if t == 'endpoint' and b['groupId'] != 'default' and ctx.group(b['groupId']):
name += f' ({ctx.group(b["groupId"]).name})'
return sentence, _unique_name(name[:80], ctx)
def _unique_name(name: str, ctx: PlatformContext) -> str:
taken = {p.name.lower() for p in ctx.policies} | ctx.policyNames
out, n = name, 2
while out.lower() in taken:
out = f'{name[:75]} ({n})'
n += 1
return out
|