@medusasec/sensitive-spans 0.1.0
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/README.md +67 -0
- package/dist/actor.js +106 -0
- package/dist/agent-policy.js +109 -0
- package/dist/attribution.js +52 -0
- package/dist/index.js +45 -0
- package/dist/local-classifier.js +499 -0
- package/dist/medusa-engine.js +720 -0
- package/dist/merge-spans.js +45 -0
- package/dist/policy-decision.js +480 -0
- package/dist/pseudonymize.js +91 -0
- package/dist/receipts.js +173 -0
- package/dist/webmcp-inventory.js +64 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @medusasec/sensitive-spans
|
|
2
|
+
|
|
3
|
+
The browser-side primitives from the [Medusa](https://github.com/joshmaster2165/medusa-agent)
|
|
4
|
+
extension, as one dependency-free ESM package. Runs in browsers, Node 20+, Deno
|
|
5
|
+
and Workers. Nothing phones home.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm i @medusasec/sensitive-spans
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What's inside
|
|
12
|
+
|
|
13
|
+
| Module | What it gives you |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `detect(text, {policy, settings, extraSpans})` | Sensitive spans (secrets, PII, financial, health, insurance, code, prompt injection) from the deterministic layer — vendor key prefixes, checksummed identifiers (Luhn, IBAN mod-97, ABA), 30+ injection families — then the **measured precision guards** and a policy decision: `{spans, action, requireJustification, disallowOverride, disallowApproval}`. |
|
|
16
|
+
| `classifyLocal`, `applyAllowlist`, `mergeSpans` | The pieces behind `detect`, if you want to run your own model and merge. |
|
|
17
|
+
| `decide`, `filterSpans`, `hasSecretSignal`, `isProsePii`, … | The guard library: each guard is traceable to a quantified false-positive reduction on a 12k-row corpus of real prose. |
|
|
18
|
+
| `detectSpansTier0` | The zero-shot Tier-0 engine (entropy scoring, base64 deobfuscation, dummy-value rejection). |
|
|
19
|
+
| `attributeActor`, `scoreSignals` | Who is driving the browser: `human \| agent \| automation \| unknown` from debugger attachment + an input fingerprint (pointer pressure, trail, unkeyed text insertion, injected-script frames). |
|
|
20
|
+
| `decideAgentAction` | Actor-aware policy: `{default, rules:[{agent, sites, actions, action}]}` → `allow \| log \| approve \| deny`. Humans are never gated. |
|
|
21
|
+
| `assignPlaceholders`, `pseudonymizeText`, `rehydrateText` | Consistent, reversible placeholders (`[PII-1]`) for detected values. |
|
|
22
|
+
| `makeReceipt`, `verifyReceipts`, `makeSigner`, `makeVerifier` | Hash-chained, ECDSA P-256 signed receipts with WebCrypto; verification works from any runtime with the public JWK. |
|
|
23
|
+
| `actorHeaderValue`, `headerRule` | The `Sec-Agent-Actor` attribution header and its per-tab declarativeNetRequest rule. |
|
|
24
|
+
| `mergeTools`, `toolsFingerprint` | WebMCP tool inventory helpers. |
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { detect, decideAgentAction, attributeActor } from "@medusasec/sensitive-spans";
|
|
30
|
+
|
|
31
|
+
const r = detect("rotate AKIAIOSFODNN7EXAMPLE before Friday", {
|
|
32
|
+
policy: { category_actions: { secret: "block" }, disallow_override: ["secret"] },
|
|
33
|
+
});
|
|
34
|
+
// r.action === "block", r.spans[0].category === "secret"
|
|
35
|
+
|
|
36
|
+
const actor = attributeActor({ attached: true, candidates: [{ id: "fcoe…", known: "claude-in-chrome", label: "Claude in Chrome", kind: "agent", enabled: true, canDrive: true }] });
|
|
37
|
+
decideAgentAction({ default: "log", rules: [{ agent: "claude-in-chrome", sites: ["*.internal.example"], action: "approve" }] }, actor, "hr.internal.example");
|
|
38
|
+
// → { action: "approve", rule: 0, source: "rule" }
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## The model
|
|
42
|
+
|
|
43
|
+
The extension also runs a 28.8 MB int8 BERT token classifier on-device. It is
|
|
44
|
+
not bundled here; pass its spans as `extraSpans` and `detect` merges them
|
|
45
|
+
before the guards run. A loader package is planned.
|
|
46
|
+
|
|
47
|
+
## Publishing
|
|
48
|
+
|
|
49
|
+
The scope is `@medusasec`; create the `medusasec` organization on npmjs.com (or
|
|
50
|
+
rename the package to your user scope) before the first publish. Then:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
npm login
|
|
54
|
+
cd packages/sensitive-spans
|
|
55
|
+
npm publish
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`prepublishOnly` rebuilds `dist/` from the extension source and runs the tests,
|
|
59
|
+
and `publishConfig.access` is `public`, so no flags are needed.
|
|
60
|
+
|
|
61
|
+
## Build
|
|
62
|
+
|
|
63
|
+
`dist/` is assembled from `../../medusa-browser-extension/src` by `build.mjs`
|
|
64
|
+
so the package can never drift from the extension. `npm test` builds and runs
|
|
65
|
+
the tests.
|
|
66
|
+
|
|
67
|
+
Apache-2.0.
|
package/dist/actor.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// Actor attribution — who is driving this tab: a person, or an agent?
|
|
2
|
+
//
|
|
3
|
+
// Two independent evidence sources, combined here (pure; unit-tested):
|
|
4
|
+
//
|
|
5
|
+
// 1. Debugger attachment. chrome.debugger.getTargets() reports `attached`
|
|
6
|
+
// for a tab whenever ANY extension or external DevTools-protocol client
|
|
7
|
+
// holds it — verified in the week-1 spikes against a mimic of Claude in
|
|
8
|
+
// Chrome and against Playwright over --remote-debugging-port. Combined
|
|
9
|
+
// with the inventory of installed driver-capable extensions this names
|
|
10
|
+
// the agent, or flags an external CDP client when none is installed.
|
|
11
|
+
//
|
|
12
|
+
// 2. Page-level input fingerprint (see driver-signals.js). Agents that never
|
|
13
|
+
// attach a debugger (a browser's own native agent, an isolated-world
|
|
14
|
+
// automation script) still produce inputs a person cannot: pointer
|
|
15
|
+
// presses with zero pressure and no movement trail, text that appears
|
|
16
|
+
// without keystrokes, DOM calls from anonymous injected scripts.
|
|
17
|
+
//
|
|
18
|
+
// Output shape is stable — it is stored on every telemetry event as
|
|
19
|
+
// metadata.actor and rendered by the dashboard.
|
|
20
|
+
|
|
21
|
+
export const ACTOR_HUMAN = "human";
|
|
22
|
+
export const ACTOR_AGENT = "agent";
|
|
23
|
+
export const ACTOR_AUTOMATION = "automation";
|
|
24
|
+
export const ACTOR_UNKNOWN = "unknown";
|
|
25
|
+
|
|
26
|
+
/** Score a driver-signals snapshot. Returns the evidence list that fired. */
|
|
27
|
+
export function scoreSignals(sig) {
|
|
28
|
+
const evidence = [];
|
|
29
|
+
if (!sig || typeof sig !== "object") return { score: 0, evidence };
|
|
30
|
+
const downs = Number(sig.pointerdowns) || 0;
|
|
31
|
+
const zero = Number(sig.zeroPressureDowns) || 0;
|
|
32
|
+
const noTrail = Number(sig.downsWithoutTrail) || 0;
|
|
33
|
+
const inputs = Number(sig.inputs) || 0;
|
|
34
|
+
const unkeyed = Number(sig.inputsWithoutKeydown) || 0;
|
|
35
|
+
const foreign = Number(sig.foreignFrames) || 0;
|
|
36
|
+
if (downs > 0 && zero / downs >= 0.8) evidence.push("pointer_pressure_zero");
|
|
37
|
+
if (downs > 0 && noTrail / downs >= 0.8) evidence.push("no_pointer_trail");
|
|
38
|
+
if (inputs > 0 && unkeyed >= Math.max(1, Math.ceil(inputs * 0.5))) evidence.push("text_inserted_without_keys");
|
|
39
|
+
if (foreign > 0) evidence.push("injected_script_frames");
|
|
40
|
+
return { score: evidence.length, evidence };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Attribute the actor for one action.
|
|
45
|
+
* @param {object} p
|
|
46
|
+
* @param {boolean} p.attached debugger attached to the tab right now
|
|
47
|
+
* @param {Array} p.candidates enabled driver-capable extensions (assessExtension shape)
|
|
48
|
+
* @param {object} p.signals driver-signals snapshot (may be null)
|
|
49
|
+
*/
|
|
50
|
+
export function attributeActor({ attached, candidates, signals } = {}) {
|
|
51
|
+
const cands = Array.isArray(candidates) ? candidates : [];
|
|
52
|
+
const sig = scoreSignals(signals);
|
|
53
|
+
|
|
54
|
+
if (attached) {
|
|
55
|
+
if (cands.length === 1) {
|
|
56
|
+
const c = cands[0];
|
|
57
|
+
return {
|
|
58
|
+
kind: c.kind === "automation" ? ACTOR_AUTOMATION : ACTOR_AGENT,
|
|
59
|
+
agent: { id: c.id, key: c.known || null, label: c.label },
|
|
60
|
+
confidence: "high",
|
|
61
|
+
evidence: ["debugger_attached", "single_driver_capable_extension", ...sig.evidence],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (cands.length > 1) {
|
|
65
|
+
return {
|
|
66
|
+
kind: ACTOR_AGENT,
|
|
67
|
+
agent: null,
|
|
68
|
+
candidates: cands.map((c) => ({ id: c.id, key: c.known || null, label: c.label })),
|
|
69
|
+
confidence: "medium",
|
|
70
|
+
evidence: ["debugger_attached", "multiple_driver_capable_extensions", ...sig.evidence],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
kind: ACTOR_AUTOMATION,
|
|
75
|
+
agent: null,
|
|
76
|
+
confidence: "high",
|
|
77
|
+
evidence: ["debugger_attached", "no_driver_capable_extension", ...sig.evidence],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (sig.score >= 2) {
|
|
82
|
+
return {
|
|
83
|
+
kind: ACTOR_AGENT,
|
|
84
|
+
agent: null,
|
|
85
|
+
confidence: sig.score >= 3 ? "medium" : "low",
|
|
86
|
+
evidence: sig.evidence,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const observed = (Number(signals?.pointerdowns) || 0) + (Number(signals?.keydowns) || 0);
|
|
90
|
+
if (observed > 0) {
|
|
91
|
+
return { kind: ACTOR_HUMAN, confidence: sig.score === 0 ? "high" : "medium", evidence: ["human_input_pattern", ...sig.evidence] };
|
|
92
|
+
}
|
|
93
|
+
// A submission with no observed input at all in the window: programmatic
|
|
94
|
+
// send or a driver we cannot see. Never call that a person.
|
|
95
|
+
return { kind: ACTOR_UNKNOWN, confidence: "low", evidence: ["no_input_observed", ...sig.evidence] };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Short human label for UI/receipts. */
|
|
99
|
+
export function actorLabel(actor) {
|
|
100
|
+
if (!actor) return "Unknown";
|
|
101
|
+
if (actor.kind === ACTOR_HUMAN) return "Human";
|
|
102
|
+
if (actor.agent && actor.agent.label) return actor.agent.label;
|
|
103
|
+
if (actor.kind === ACTOR_AUTOMATION) return "External automation (CDP client)";
|
|
104
|
+
if (actor.kind === ACTOR_AGENT) return "Unidentified agent";
|
|
105
|
+
return "Unknown";
|
|
106
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Actor-aware policy: what may an AGENT do on this site?
|
|
2
|
+
//
|
|
3
|
+
// The org policy's `extension.agents` block:
|
|
4
|
+
// {
|
|
5
|
+
// default: "allow" | "log" | "approve" | "deny", // no rule matched
|
|
6
|
+
// rules: [{
|
|
7
|
+
// agent: "*" | "<known key, e.g. claude-in-chrome>" | "<extension id>"
|
|
8
|
+
// | "automation" (external CDP client) | "unidentified" (agent, unnamed),
|
|
9
|
+
// sites: ["*", "chatgpt.com", "*.example.com"],
|
|
10
|
+
// actions: ["*", "prompt_submit", "file_upload"],
|
|
11
|
+
// action: "allow" | "log" | "approve" | "deny",
|
|
12
|
+
// }],
|
|
13
|
+
// }
|
|
14
|
+
//
|
|
15
|
+
// Semantics (content DLP policy still applies on top in every case):
|
|
16
|
+
// allow — agent actions are treated like a person's.
|
|
17
|
+
// log — every agent-driven submission is recorded (even a clean one) and
|
|
18
|
+
// receipted; nothing is blocked.
|
|
19
|
+
// approve — the submission is HELD until an admin approves it (existing
|
|
20
|
+
// approvals flow, TTL-bounded), whatever its content.
|
|
21
|
+
// deny — agent-driven submissions on this site are refused outright. A
|
|
22
|
+
// person taking the keyboard back can still send.
|
|
23
|
+
//
|
|
24
|
+
// Human actors are never gated here. First matching rule wins. Pure module,
|
|
25
|
+
// unit-tested; the service worker and the propagation test both import it.
|
|
26
|
+
|
|
27
|
+
export const AGENT_ACTIONS = ["allow", "log", "approve", "deny"];
|
|
28
|
+
export const DEFAULT_AGENTS_POLICY = Object.freeze({ default: "allow", rules: [] });
|
|
29
|
+
|
|
30
|
+
const RANK = { allow: 0, log: 1, approve: 2, deny: 3 };
|
|
31
|
+
|
|
32
|
+
function normalizeAction(a, fallback = "allow") {
|
|
33
|
+
const s = String(a || "").toLowerCase();
|
|
34
|
+
return AGENT_ACTIONS.includes(s) ? s : fallback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The agent selector keys an actor answers to, most specific first. */
|
|
38
|
+
export function actorKeys(actor) {
|
|
39
|
+
if (!actor || actor.kind === "human") return [];
|
|
40
|
+
const keys = [];
|
|
41
|
+
if (actor.agent) {
|
|
42
|
+
if (actor.agent.key) keys.push(String(actor.agent.key).toLowerCase());
|
|
43
|
+
if (actor.agent.id) keys.push(String(actor.agent.id).toLowerCase());
|
|
44
|
+
}
|
|
45
|
+
if (actor.kind === "automation") keys.push("automation");
|
|
46
|
+
if (actor.kind === "agent" && !actor.agent) keys.push("unidentified");
|
|
47
|
+
if (actor.kind === "unknown") keys.push("unknown");
|
|
48
|
+
keys.push("*");
|
|
49
|
+
return keys;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function siteMatches(pattern, host) {
|
|
53
|
+
const p = String(pattern || "").trim().toLowerCase();
|
|
54
|
+
const h = String(host || "").trim().toLowerCase();
|
|
55
|
+
if (!p) return false;
|
|
56
|
+
if (p === "*") return true;
|
|
57
|
+
if (!h) return false;
|
|
58
|
+
if (p.startsWith("*.")) {
|
|
59
|
+
const base = p.slice(2);
|
|
60
|
+
return h === base || h.endsWith("." + base);
|
|
61
|
+
}
|
|
62
|
+
return h === p;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function ruleMatches(rule, actor, host, actionKind) {
|
|
66
|
+
if (!rule || typeof rule !== "object") return false;
|
|
67
|
+
const keys = actorKeys(actor);
|
|
68
|
+
if (!keys.length) return false;
|
|
69
|
+
const agentSel = String(rule.agent || "*").toLowerCase();
|
|
70
|
+
if (!keys.includes(agentSel)) return false;
|
|
71
|
+
const sites = Array.isArray(rule.sites) && rule.sites.length ? rule.sites : ["*"];
|
|
72
|
+
if (!sites.some((s) => siteMatches(s, host))) return false;
|
|
73
|
+
const actions = Array.isArray(rule.actions) && rule.actions.length ? rule.actions : ["*"];
|
|
74
|
+
const kind = String(actionKind || "prompt_submit").toLowerCase();
|
|
75
|
+
return actions.some((a) => a === "*" || String(a).toLowerCase() === kind);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Decide the gate for one action.
|
|
80
|
+
* @returns {{action:"allow"|"log"|"approve"|"deny", rule:number|null, source:"rule"|"default"|"human"|"none"}}
|
|
81
|
+
*/
|
|
82
|
+
export function decideAgentAction(agentsPolicy, actor, host, actionKind = "prompt_submit") {
|
|
83
|
+
if (!actor || actor.kind === "human") return { action: "allow", rule: null, source: "human" };
|
|
84
|
+
// `unknown` (no input observed at all) is gated only by rules that name it
|
|
85
|
+
// explicitly — never by "*" or the default, so a quiet programmatic send is
|
|
86
|
+
// not mistaken for an agent.
|
|
87
|
+
const pol = agentsPolicy && typeof agentsPolicy === "object" ? agentsPolicy : DEFAULT_AGENTS_POLICY;
|
|
88
|
+
const rules = Array.isArray(pol.rules) ? pol.rules : [];
|
|
89
|
+
for (let i = 0; i < rules.length; i++) {
|
|
90
|
+
const r = rules[i];
|
|
91
|
+
if (actor.kind === "unknown" && String(r?.agent || "*").toLowerCase() !== "unknown") continue;
|
|
92
|
+
if (ruleMatches(r, actor, host, actionKind)) {
|
|
93
|
+
return { action: normalizeAction(r.action), rule: i, source: "rule" };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (actor.kind === "unknown") return { action: "allow", rule: null, source: "none" };
|
|
97
|
+
return { action: normalizeAction(pol.default), rule: null, source: "default" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Strictest of two gates (used to aggregate a multi-file upload). */
|
|
101
|
+
export function strictestGate(a, b) {
|
|
102
|
+
if (!a) return b || null;
|
|
103
|
+
if (!b) return a;
|
|
104
|
+
return (RANK[b.action] || 0) > (RANK[a.action] || 0) ? b : a;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function isGated(gate) {
|
|
108
|
+
return !!gate && (gate.action === "approve" || gate.action === "deny");
|
|
109
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Agent attribution header — lets the org's own sites, WAF and SaaS tell
|
|
2
|
+
// agent-driven traffic from a person's, per tab, in real time.
|
|
3
|
+
//
|
|
4
|
+
// When a tab is agent-driven and the org policy enables
|
|
5
|
+
// `agents.attribution_header`, the service worker installs a
|
|
6
|
+
// declarativeNetRequest SESSION rule scoped to that tab that sets
|
|
7
|
+
// Sec-Agent-Actor: <agent-key>; kind=<kind>; confidence=<c>; endpoint=<id>
|
|
8
|
+
// on every request the tab makes to a site Medusa has host access to. The rule
|
|
9
|
+
// is removed when the agent detaches or the tab closes. Verified in the week-1
|
|
10
|
+
// spikes (fetch + top-level navigation; other tabs untouched).
|
|
11
|
+
//
|
|
12
|
+
// Pure helpers here; the chrome.declarativeNetRequest calls live in
|
|
13
|
+
// agent-governance.js.
|
|
14
|
+
|
|
15
|
+
export const ACTOR_HEADER = "Sec-Agent-Actor";
|
|
16
|
+
const RULE_BASE = 1_000_000;
|
|
17
|
+
|
|
18
|
+
// Header values are restricted to RFC 9110 token-ish characters.
|
|
19
|
+
function tok(s, max = 64) {
|
|
20
|
+
return String(s == null ? "" : s).replace(/[^A-Za-z0-9._:-]/g, "-").slice(0, max) || "unknown";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function actorHeaderValue(actor, endpoint) {
|
|
24
|
+
const key = actor?.agent?.key || actor?.agent?.id || (actor?.kind === "automation" ? "automation" : actor?.kind === "agent" ? "unidentified" : "unknown");
|
|
25
|
+
const parts = [
|
|
26
|
+
tok(key),
|
|
27
|
+
`kind=${tok(actor?.kind || "unknown", 16)}`,
|
|
28
|
+
`confidence=${tok(actor?.confidence || "low", 8)}`,
|
|
29
|
+
`endpoint=${tok(endpoint || "unenrolled", 80)}`,
|
|
30
|
+
];
|
|
31
|
+
return parts.join("; ");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function headerRuleId(tabId) {
|
|
35
|
+
const n = Number(tabId) || 0;
|
|
36
|
+
return RULE_BASE + (n % 1_000_000_000);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function headerRule(tabId, value) {
|
|
40
|
+
return {
|
|
41
|
+
id: headerRuleId(tabId),
|
|
42
|
+
priority: 1,
|
|
43
|
+
action: {
|
|
44
|
+
type: "modifyHeaders",
|
|
45
|
+
requestHeaders: [{ header: ACTOR_HEADER, operation: "set", value }],
|
|
46
|
+
},
|
|
47
|
+
condition: {
|
|
48
|
+
tabIds: [Number(tabId)],
|
|
49
|
+
resourceTypes: ["main_frame", "sub_frame", "xmlhttprequest", "ping", "other", "script", "image", "stylesheet", "font", "media", "websocket"],
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// @medusasec/sensitive-spans — assembled by build.mjs from the Medusa extension.
|
|
2
|
+
export { classifyLocal, applyAllowlist } from "./local-classifier.js";
|
|
3
|
+
export {
|
|
4
|
+
decide, filterSpans, actionForSpans, cleanSpanText,
|
|
5
|
+
hasGenuineIdentifier, hasSecretSignal, hasCodeSignal, hasHealthSignal,
|
|
6
|
+
hasFinancialSignal, hasInsuranceSignal, isProsePii, isDegenerateSpan,
|
|
7
|
+
isNoisyPii, isNoisyOcrText, isObserveMode, isFailClosed, unscannableAction,
|
|
8
|
+
} from "./policy-decision.js";
|
|
9
|
+
export { mergeSpans } from "./merge-spans.js";
|
|
10
|
+
export { detectSpans as detectSpansTier0, DEFAULT_POLICY as TIER0_DEFAULT_POLICY, PRODUCTION_CATEGORIES as TIER0_CATEGORIES } from "./medusa-engine.js";
|
|
11
|
+
export * from "./actor.js";
|
|
12
|
+
export * from "./agent-policy.js";
|
|
13
|
+
export * from "./receipts.js";
|
|
14
|
+
export * from "./pseudonymize.js";
|
|
15
|
+
export * from "./attribution.js";
|
|
16
|
+
export * from "./webmcp-inventory.js";
|
|
17
|
+
|
|
18
|
+
import { classifyLocal as _classifyLocal, applyAllowlist as _applyAllowlist } from "./local-classifier.js";
|
|
19
|
+
import { decide as _decide } from "./policy-decision.js";
|
|
20
|
+
|
|
21
|
+
/** Every category on, no confidence floor — the enrolled-endpoint posture. */
|
|
22
|
+
export const ALL_CATEGORIES_ON = Object.freeze({
|
|
23
|
+
SECRET: true, PII: true, FINANCIAL: true, HEALTH: true, INSURANCE: true, CODE: true, INJECTION: true,
|
|
24
|
+
LEGAL: true, HR: true, BUSINESS: true,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detect sensitive spans in `text` with the deterministic layer (vendor key
|
|
29
|
+
* patterns, checksummed PII/financial identifiers, prompt-injection families,
|
|
30
|
+
* your custom patterns) and run the measured precision guards + policy over
|
|
31
|
+
* them. Returns { spans, action, requireJustification, disallowOverride,
|
|
32
|
+
* disallowApproval } exactly as the extension decides it.
|
|
33
|
+
*
|
|
34
|
+
* Plug a model in by passing `extraSpans` (e.g. from your own token
|
|
35
|
+
* classifier); they are merged before the guards run.
|
|
36
|
+
*/
|
|
37
|
+
export function detect(text, { policy = {}, settings, extraSpans = [] } = {}) {
|
|
38
|
+
const s = settings || { enabledCategories: ALL_CATEGORIES_ON, confidenceThreshold: 0, minTextLength: 1 };
|
|
39
|
+
const local = _classifyLocal(String(text ?? ""), {
|
|
40
|
+
customPatterns: policy.custom_dlp_patterns,
|
|
41
|
+
disabledDetectors: policy.disabled_detectors,
|
|
42
|
+
});
|
|
43
|
+
const merged = _applyAllowlist([...(extraSpans || []), ...local], policy.dlp_allowlist);
|
|
44
|
+
return _decide(merged, s, policy, String(text ?? ""));
|
|
45
|
+
}
|