openscript-sdk 0.1.0__py3-none-any.whl
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.
- openscript_sdk/__init__.py +217 -0
- openscript_sdk/contracts/__init__.py +82 -0
- openscript_sdk/contracts/interceptor.py +98 -0
- openscript_sdk/contracts/server_types.py +87 -0
- openscript_sdk/contracts/session_graph.py +123 -0
- openscript_sdk/contracts/types.py +76 -0
- openscript_sdk/events/__init__.py +0 -0
- openscript_sdk/events/approvals.py +309 -0
- openscript_sdk/events/models.py +26 -0
- openscript_sdk/events/store.py +79 -0
- openscript_sdk/events/writer.py +100 -0
- openscript_sdk/hooks/claude_code.py +479 -0
- openscript_sdk/hooks/claude_code_example.yaml +62 -0
- openscript_sdk/hooks/claude_code_strict_example.yaml +73 -0
- openscript_sdk/hooks/config.py +144 -0
- openscript_sdk/hooks/settings_example.json +28 -0
- openscript_sdk/hooks/settings_strict_example.json +28 -0
- openscript_sdk/integrations/langchain.py +32 -0
- openscript_sdk/integrations/langgraph.py +32 -0
- openscript_sdk/interceptors/base.py +15 -0
- openscript_sdk/interceptors/event_writer.py +131 -0
- openscript_sdk/interceptors/pii.py +168 -0
- openscript_sdk/interceptors/threat.py +674 -0
- openscript_sdk/logging.py +94 -0
- openscript_sdk/middleware/middleware.py +543 -0
- openscript_sdk/middleware/tool.py +247 -0
- openscript_sdk/observability/__init__.py +11 -0
- openscript_sdk/observability/metrics.py +105 -0
- openscript_sdk/observability/risk.py +54 -0
- openscript_sdk/observability/tracing.py +67 -0
- openscript_sdk/policies/compliance.py +261 -0
- openscript_sdk/policies/config.py +244 -0
- openscript_sdk/policies/harmful_request.py +617 -0
- openscript_sdk/policies/normalize.py +489 -0
- openscript_sdk/policies/output_schema.py +353 -0
- openscript_sdk/policies/policies_example.yaml +36 -0
- openscript_sdk/policies/redaction.py +359 -0
- openscript_sdk/policies/secrets.py +239 -0
- openscript_sdk/policies/semantic.py +348 -0
- openscript_sdk/policies/semantic_head.py +9 -0
- openscript_sdk/policies/session_taint.py +399 -0
- openscript_sdk/policies/taint_store.py +111 -0
- openscript_sdk/policies/tool_firewall.py +434 -0
- openscript_sdk/policies/tools_example.yaml +81 -0
- openscript_sdk/policies/toxicity.py +247 -0
- openscript_sdk/py.typed +0 -0
- openscript_sdk/server/__init__.py +1 -0
- openscript_sdk/server/app.py +153 -0
- openscript_sdk/server/auth.py +19 -0
- openscript_sdk/server/guard.py +126 -0
- openscript_sdk/server/guard_example.yaml +55 -0
- openscript_sdk/server/routers/__init__.py +0 -0
- openscript_sdk/server/routers/approvals.py +69 -0
- openscript_sdk/server/routers/guard.py +231 -0
- openscript_sdk/server/routers/sessions.py +80 -0
- openscript_sdk/server/routers/sse.py +69 -0
- openscript_sdk/server/routers/threat.py +119 -0
- openscript_sdk/server/static/dashboard/index.html +209 -0
- openscript_sdk/server/static/dashboard/session.html +339 -0
- openscript_sdk-0.1.0.dist-info/METADATA +225 -0
- openscript_sdk-0.1.0.dist-info/RECORD +65 -0
- openscript_sdk-0.1.0.dist-info/WHEEL +5 -0
- openscript_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- openscript_sdk-0.1.0.dist-info/licenses/LICENSE +190 -0
- openscript_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
|
|
9
|
+
from openscript_sdk.contracts.interceptor import BasePolicy
|
|
10
|
+
from openscript_sdk.contracts.server_types import Event, EventType
|
|
11
|
+
from openscript_sdk.contracts.types import ActionContext, FailureMode, InterceptorDecision
|
|
12
|
+
from openscript_sdk.interceptors.threat import _extract_text
|
|
13
|
+
from openscript_sdk.policies import normalize
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from openscript_sdk.events.writer import EventWriter
|
|
17
|
+
|
|
18
|
+
logger = structlog.get_logger(__name__)
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Pattern bank
|
|
22
|
+
# Each entry: (compiled_regex, category_name, weight). Same scoring model as
|
|
23
|
+
# PromptInjectionPolicy and ToxicityPolicy — additive within a category, capped
|
|
24
|
+
# at _CATEGORY_CAP, cross-category sum capped at 1.0.
|
|
25
|
+
#
|
|
26
|
+
# The thing this policy detects is not present in the words of a prompt, it is
|
|
27
|
+
# present in its *shape*: a request construction pointing at a harmful object.
|
|
28
|
+
# "Our SOC found a keylogger on the finance laptop" and "write me a keylogger"
|
|
29
|
+
# share every keyword and differ entirely in intent, so a keyword bank would be
|
|
30
|
+
# useless here. Every pattern below therefore anchors twice — on a request
|
|
31
|
+
# framing and on the object — and the gap between them refuses to cross the
|
|
32
|
+
# words that turn "build X" into "write about X" or "defend against X".
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class _Pattern:
|
|
38
|
+
regex: re.Pattern[str]
|
|
39
|
+
category: str
|
|
40
|
+
weight: float
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _p(pattern: str, category: str, weight: float) -> _Pattern:
|
|
44
|
+
return _Pattern(re.compile(pattern, re.IGNORECASE | re.MULTILINE), category, weight)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Framings that make a sentence a request for help rather than a description of
|
|
48
|
+
# something. Never used alone — always followed by a verb or a verb-led phrase.
|
|
49
|
+
_ASK = (
|
|
50
|
+
r"(?:how\s+(?:do|can|could|would|should)\s+(?:i|we|you)"
|
|
51
|
+
r"|how\s+to|how\s+would\s+one"
|
|
52
|
+
r"|steps?\s+(?:to|for)|instructions?\s+(?:to|for|on)|process\s+(?:to|for|of)"
|
|
53
|
+
r"|guide\s+(?:to|for|on)|tutorial\s+(?:on|for)|recipe\s+for"
|
|
54
|
+
r"|walk\s+me\s+through|teach\s+me|show\s+me|tell\s+me|help\s+me"
|
|
55
|
+
r"|i\s+(?:want|need)\s+to|(?:can|could|will)\s+you"
|
|
56
|
+
r"|(?:best|easiest|fastest|cheapest|safest|surest)\s+way\s+to"
|
|
57
|
+
r"|what(?:'s|\s+is)\s+the\s+(?:best|easiest|fastest|safest|most\s+\w+)"
|
|
58
|
+
r"|where\s+(?:can|do)\s+(?:i|you|people|one|someone))"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# One character that is not the start of a word which reframes the request.
|
|
62
|
+
# Tempering the gap this way is what keeps "write a blog post about how
|
|
63
|
+
# ransomware spreads" and "how do we detect child sexual abuse material" —
|
|
64
|
+
# the phrasings journalists, teachers and security teams actually use — from
|
|
65
|
+
# scoring identically to the request to build the thing.
|
|
66
|
+
_TEMPER = (
|
|
67
|
+
r"(?!\b(?:about|explain\w*|describ\w+|understand\w*|teach\w*\s+students"
|
|
68
|
+
r"|detect\w*|remov\w+|prevent\w*|defen[ds]\w*|protect\w*|mitigat\w+"
|
|
69
|
+
r"|respond\w*|recover\w*|report\w*|investigat\w+|audit\w*|review\w*"
|
|
70
|
+
r"|blog|article|essay|post|paper|thesis|book|course|lesson|lecture"
|
|
71
|
+
r"|training|awareness|curriculum|policy|presentation|talk|deck"
|
|
72
|
+
r"|signature|scanner|detector|honeypot|simulation|exercise|drill"
|
|
73
|
+
r"|bath|photo)\b)."
|
|
74
|
+
)
|
|
75
|
+
_GAP = rf"(?:{_TEMPER}){{0,40}}?"
|
|
76
|
+
_LEAD = rf"(?:{_TEMPER}){{0,20}}?"
|
|
77
|
+
|
|
78
|
+
# Sentence-initial imperative, or one of the framings above. Whatever follows a
|
|
79
|
+
# _REQ has to begin with a verb — that is the whole reason this does not match
|
|
80
|
+
# every line of every document.
|
|
81
|
+
_REQ = rf"(?:^\s*(?:please[,\s]+)?|(?<=[.!?]\s)|{_ASK}{_LEAD})"
|
|
82
|
+
|
|
83
|
+
# Verbs that ask for the thing to be produced or handed over.
|
|
84
|
+
_PRODUCE = (
|
|
85
|
+
r"(?:writ\w+|draft\w*|build\w*|mak\w+|creat\w+|construct\w*|assembl\w+|cod\w+"
|
|
86
|
+
r"|program\w*|develop\w*|generat\w+|design\w*|synthesi[sz]\w+"
|
|
87
|
+
r"|manufactur\w+|machin\w+|cook\w*|brew\w*|3d[\s-]?print\w*|print\w*|forge\w*"
|
|
88
|
+
r"|convert\w*|modif\w+|craft\w*|set\s+up|put\s+together"
|
|
89
|
+
r"|giv\w+|provid\w+|send|hand)"
|
|
90
|
+
)
|
|
91
|
+
# Verbs that ask for the thing to be obtained.
|
|
92
|
+
_OBTAIN = (
|
|
93
|
+
r"(?:buy\w*|purchas\w+|acquir\w+|obtain\w*|order|get|find|source"
|
|
94
|
+
r"|smuggl\w+|import\w*|steal|hire|pay\s+someone)"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _produce(obj: str, category: str, weight: float) -> _Pattern:
|
|
99
|
+
"""A request to build, write or hand over *obj*."""
|
|
100
|
+
return _p(rf"{_REQ}{_PRODUCE}(?:\s+me)?\b{_GAP}\b(?:{obj})", category, weight)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _obtain(obj: str, category: str, weight: float) -> _Pattern:
|
|
104
|
+
"""A request to acquire *obj*."""
|
|
105
|
+
return _p(rf"{_REQ}{_OBTAIN}(?:\s+me)?\b{_GAP}\b(?:{obj})", category, weight)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _act(phrase: str, category: str, weight: float) -> _Pattern:
|
|
109
|
+
"""A request for a harmful act. *phrase* must begin with its own verb."""
|
|
110
|
+
return _p(rf"{_REQ}(?:{phrase})", category, weight)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# --- objects -----------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
_FIREARM = (
|
|
116
|
+
r"ghost\s+guns?|untraceable\s+(?:guns?|firearms?|weapons?|receivers?)"
|
|
117
|
+
r"|unserialized\s+(?:receivers?|firearms?|lowers?)"
|
|
118
|
+
r"|(?:80|eighty)\s*%\s*(?:lower|receiver)"
|
|
119
|
+
r"|auto\s+sear|drop[\s-]?in\s+auto"
|
|
120
|
+
r"|semi[\s-]?auto\w*\s+(?:in)?to\s+(?:a\s+)?full[\s-]?auto"
|
|
121
|
+
r"|(?:gun|firearm|rifle|pistol|silencer|suppressor)\s+at\s+home"
|
|
122
|
+
r"|unregistered\s+(?:firearms?|receivers?|lowers?|guns?)"
|
|
123
|
+
r"|(?:firearm|gun|rifle)\s+receivers?\s+at\s+home"
|
|
124
|
+
)
|
|
125
|
+
_EXPLOSIVE = (
|
|
126
|
+
r"(?:pipe|car|nail|letter|dirty|fertilizer|pressure\s+cooker)\s+bombs?"
|
|
127
|
+
r"|(?<!bath )(?<!photo )bombs?\b(?![\s-]*proof)|explosive\s+devices?|detonators?"
|
|
128
|
+
r"|napalm|thermite|c-?4\b|tatp|improvised\s+explosives?|ammonium\s+nitrate|anfo\b"
|
|
129
|
+
)
|
|
130
|
+
_CBRN = (
|
|
131
|
+
r"weapons?\s+of\s+mass\s+destruction|wmds?\b"
|
|
132
|
+
r"|(?:nuclear|atomic|nerve|chemical|biological|radiological)\s+"
|
|
133
|
+
r"(?:bombs?|weapons?|agents?|devices?)"
|
|
134
|
+
r"|bio[\s-]?weapons?|sarin|vx\s+(?:gas|nerve)|mustard\s+gas|phosgene"
|
|
135
|
+
r"|ricin|anthrax|botulinum|toxic\s+gas|poison(?:ous)?\s+gas|chlorine\s+gas"
|
|
136
|
+
r"|weaponi[sz]\w+\s+(?:a\s+)?(?:virus|pathogen|anthrax|strain)"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
_MALWARE = (
|
|
140
|
+
r"key\s?logger\w*|ransomware|rootkits?|botnets?|trojans?|cryptolocker"
|
|
141
|
+
r"|(?:computer\s+)?virus\s+(?:to|that|which)\b|worms?\s+(?:that|to)\b"
|
|
142
|
+
r"|malware\s+(?:that|to)\b|c2\s+servers?|command\s+and\s+control\s+server"
|
|
143
|
+
r"|reverse\s+shell|wiper\s+malware"
|
|
144
|
+
r"|record\w*\s+(?:every\s+|all\s+|each\s+)?keystrokes?|captur\w+\s+keystrokes?"
|
|
145
|
+
r"|encrypts?\s+(?:\w+\s+){0,4}?files?\s+(?:\w+\s+){0,6}?(?:demands?|ransom|payment)"
|
|
146
|
+
)
|
|
147
|
+
_OFFENSIVE = (
|
|
148
|
+
r"phishing\s+(?:pages?|sites?|kits?|emails?|campaigns?|links?)"
|
|
149
|
+
r"|credential\s+stuffing|password\s+crackers?"
|
|
150
|
+
r"|ddos|denial[\s-]of[\s-]service"
|
|
151
|
+
r"|sql\s+injection\s+(?:payloads?|attacks?|strings?)"
|
|
152
|
+
r"|zero[\s-]?day\s+exploits?|exploit\s+for\s+cve"
|
|
153
|
+
r"|backdoors?\s+(?:into|in|on)\b"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
_ID_GOV = (
|
|
157
|
+
r"social\s+security\s+(?:number|#)|ssn\b|tax\s+id|passport\s+numbers?"
|
|
158
|
+
r"|driver'?s?\s+licen[cs]e\s+numbers?|medical\s+records?|health\s+records?"
|
|
159
|
+
r"|bank\s+account\s+(?:number|details)|credit\s+card\s+numbers?|credit\s+score"
|
|
160
|
+
)
|
|
161
|
+
_ID_LOC = (
|
|
162
|
+
r"home\s+address(?:es)?|house\s+address(?:es)?|home\s+phone"
|
|
163
|
+
r"|personal\s+(?:phone|email|cell|number|address)"
|
|
164
|
+
r"|private\s+(?:phone|email|number|address)"
|
|
165
|
+
r"|phone\s+numbers?|cell\s+(?:phone\s+)?numbers?|whereabouts|maiden\s+name"
|
|
166
|
+
)
|
|
167
|
+
_THIRD = (
|
|
168
|
+
r"(?:his|her|their|(?:some|any)(?:one|body)(?:'s|s')?"
|
|
169
|
+
r"|my\s+(?:boss|supervisor|manager|coworkers?|co-workers?|colleagues?"
|
|
170
|
+
r"|neighbou?rs?|ex|ex-\w+|teachers?|professors?|landlords?|tenants?"
|
|
171
|
+
r"|clients?|patients?|students?|wife|husband|girlfriend|boyfriend"
|
|
172
|
+
r"|partner|roommates?)(?:'s|s')?"
|
|
173
|
+
r"|the\s+(?:ceo|cfo|cto|coo|president|chairman|founder|journalists?"
|
|
174
|
+
r"|reporters?|woman|man|guy|girl|person|employees?|customers?|patients?"
|
|
175
|
+
r"|students?|teachers?|witness|victim|plaintiff|defendant)(?:'s|s')?"
|
|
176
|
+
r"|(?:mr|ms|mrs|dr)\.?\s+\w+(?:'s|s')?)"
|
|
177
|
+
)
|
|
178
|
+
_LOOKUP = (
|
|
179
|
+
r"(?:what(?:'s|\s+is|\s+are)|find|get|look\s*up|locate|track\s+down"
|
|
180
|
+
r"|dig\s+up|tell\s+me|giv\w+\s+me|show\s+me|pull\s+up|i\s+need|search\s+for"
|
|
181
|
+
r"|how\s+(?:do|can)\s+i\s+(?:find|get|look\s*up|locate))"
|
|
182
|
+
)
|
|
183
|
+
_NEAR = rf"(?:{_TEMPER}){{0,25}}?"
|
|
184
|
+
_ASK_PII = rf"(?:^\s*|(?<=[.!?]\s)|{_ASK}{_LEAD}){_LOOKUP}"
|
|
185
|
+
|
|
186
|
+
_DRUG_STREET = (
|
|
187
|
+
r"fentanyl|heroin|cocaine|crack\s+cocaine|methamphetamine|meth\b"
|
|
188
|
+
r"|crystal\s+meth|mdma|ecstasy|lsd\b|ghb\b|rohypnol|roofies"
|
|
189
|
+
)
|
|
190
|
+
_DRUG_RX = (
|
|
191
|
+
r"oxycodone|oxycontin|percocet|vicodin|adderall|xanax|valium" r"|ketamine|codeine|opioids?"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
_PATTERNS: list[_Pattern] = [
|
|
196
|
+
# Weapons — manufacture, conversion, and CBRN
|
|
197
|
+
_produce(_FIREARM, "weapons", 0.65),
|
|
198
|
+
_produce(_EXPLOSIVE, "weapons", 0.65),
|
|
199
|
+
_produce(_CBRN, "weapons", 0.70),
|
|
200
|
+
_obtain(_CBRN, "weapons", 0.65),
|
|
201
|
+
_act(r"convert\w*\s+(?:a\s+)?semi[\s-]?auto\w*", "weapons", 0.60),
|
|
202
|
+
# Malware and offensive tooling
|
|
203
|
+
_produce(_MALWARE, "malware", 0.60),
|
|
204
|
+
_produce(_OFFENSIVE, "malware", 0.60),
|
|
205
|
+
_act(
|
|
206
|
+
r"brute[\s-]?forc\w+\s+(?:\w+\s+){0,3}?(?:passwords?|wifi|logins?|accounts?|hashes?)",
|
|
207
|
+
"malware",
|
|
208
|
+
0.60,
|
|
209
|
+
),
|
|
210
|
+
_act(
|
|
211
|
+
r"hack\s+(?:in)?to\s+(?:\w+\s+){0,3}?"
|
|
212
|
+
r"(?:accounts?|phones?|emails?|instagram|facebook|snapchat|wifi|network)",
|
|
213
|
+
"malware",
|
|
214
|
+
0.55,
|
|
215
|
+
),
|
|
216
|
+
# Soliciting a third party's identifiers or records
|
|
217
|
+
_p(rf"{_ASK_PII}{_NEAR}\b{_THIRD}{_NEAR}\b(?:{_ID_GOV})", "pii_solicitation", 0.65),
|
|
218
|
+
_p(
|
|
219
|
+
rf"{_ASK_PII}{_NEAR}\b(?:{_ID_GOV}){_NEAR}\b(?:of|for|belonging\s+to)\s+{_THIRD}",
|
|
220
|
+
"pii_solicitation",
|
|
221
|
+
0.65,
|
|
222
|
+
),
|
|
223
|
+
_p(rf"{_ASK_PII}{_NEAR}\b{_THIRD}{_NEAR}\b(?:{_ID_LOC})", "pii_solicitation", 0.55),
|
|
224
|
+
_p(
|
|
225
|
+
rf"{_ASK_PII}{_NEAR}\b(?:{_ID_LOC}){_NEAR}\b(?:of|for|belonging\s+to)\s+{_THIRD}",
|
|
226
|
+
"pii_solicitation",
|
|
227
|
+
0.55,
|
|
228
|
+
),
|
|
229
|
+
_act(
|
|
230
|
+
r"(?:find|figur\w+)\s+out\s+(?:where|who)\s+(?:\w+\s+){0,3}?"
|
|
231
|
+
r"(?:lives?|works?|really\s+is|is\s+staying)",
|
|
232
|
+
"pii_solicitation",
|
|
233
|
+
0.55,
|
|
234
|
+
),
|
|
235
|
+
# Acquiring controlled goods or criminal services
|
|
236
|
+
_obtain(_DRUG_STREET, "illicit_acquisition", 0.55),
|
|
237
|
+
_produce(_DRUG_STREET, "illicit_acquisition", 0.60),
|
|
238
|
+
_act(
|
|
239
|
+
rf"(?:get|buy|obtain|order)\s+(?:{_DRUG_RX})\b{_GAP}"
|
|
240
|
+
r"\bwithout\s+a\s+(?:prescription|script|doctor)",
|
|
241
|
+
"illicit_acquisition",
|
|
242
|
+
0.55,
|
|
243
|
+
),
|
|
244
|
+
_obtain(
|
|
245
|
+
r"(?:an?\s+)?(?:guns?|firearms?|pistols?|rifles?)\b.{0,25}?"
|
|
246
|
+
r"\bwithout\s+a\s+(?:background\s+check|permit|licen[cs]e|ffl)",
|
|
247
|
+
"illicit_acquisition",
|
|
248
|
+
0.60,
|
|
249
|
+
),
|
|
250
|
+
_obtain(
|
|
251
|
+
r"stolen\s+(?:credit\s+cards?|cards?|accounts?|credentials?|data|logins?|identit\w+)"
|
|
252
|
+
r"|dumps\s+with\s+pins?|fullz\b",
|
|
253
|
+
"illicit_acquisition",
|
|
254
|
+
0.60,
|
|
255
|
+
),
|
|
256
|
+
_obtain(
|
|
257
|
+
r"(?:an?\s+)?(?:fake|forged|counterfeit)\s+"
|
|
258
|
+
r"(?:ids?|passports?|driver'?s?\s+licen[cs]es?|documents?)",
|
|
259
|
+
"illicit_acquisition",
|
|
260
|
+
0.60,
|
|
261
|
+
),
|
|
262
|
+
_act(
|
|
263
|
+
r"(?:hire|find|pay)\s+(?:someone|a\s+(?:hacker|hitman|hit\s+man))\s+to\s+"
|
|
264
|
+
r"(?:hack|kill|hurt|beat|break|steal)",
|
|
265
|
+
"illicit_acquisition",
|
|
266
|
+
0.65,
|
|
267
|
+
),
|
|
268
|
+
# Financial crime
|
|
269
|
+
_act(r"launder\w*\s+(?:the\s+)?(?:money|cash|funds|proceeds)", "fraud", 0.60),
|
|
270
|
+
_produce(
|
|
271
|
+
r"counterfeit\s+(?:\w+\s+){0,2}?(?:bills?|money|currency|banknotes?|notes?)"
|
|
272
|
+
r"|fake\s+(?:\w+\s+){0,2}?(?:bills?|currency|banknotes?)",
|
|
273
|
+
"fraud",
|
|
274
|
+
0.60,
|
|
275
|
+
),
|
|
276
|
+
_act(
|
|
277
|
+
r"steal\s+(?:someone'?s?|somebody'?s?|his|her|their|a\s+person'?s?|my\s+\w+'?s?)\s+"
|
|
278
|
+
r"identity|commit\s+identity\s+theft",
|
|
279
|
+
"fraud",
|
|
280
|
+
0.60,
|
|
281
|
+
),
|
|
282
|
+
_act(
|
|
283
|
+
r"(?:kite|kiting)\s+checks?|check\s+kiting"
|
|
284
|
+
r"|writ\w+\s+(?:a\s+)?(?:bad|rubber|fraudulent|hot)\s+che(?:ck|que)"
|
|
285
|
+
r"|writ\w+\s+(?:a\s+)?che(?:ck|que)\s+that\s+(?:wo|do)n'?t\s+(?:bounce|clear)",
|
|
286
|
+
"fraud",
|
|
287
|
+
0.55,
|
|
288
|
+
),
|
|
289
|
+
_act(
|
|
290
|
+
r"(?:evade|dodge)\s+(?:paying\s+)?(?:taxes|the\s+irs)|tax\s+evasion\s+scheme", "fraud", 0.50
|
|
291
|
+
),
|
|
292
|
+
# Planning harm to a person
|
|
293
|
+
_act(
|
|
294
|
+
r"poison\s+(?:someone|somebody|a\s+person|him|her|them|my\s+\w+)"
|
|
295
|
+
r"|(?:best|undetectable|untraceable)\s+poison"
|
|
296
|
+
r"|poisons?\s+to\s+(?:use\s+)?on\b",
|
|
297
|
+
"violence_planning",
|
|
298
|
+
0.65,
|
|
299
|
+
),
|
|
300
|
+
_act(
|
|
301
|
+
r"(?:hurt|harm|injur\w+|beat|attack)\s+"
|
|
302
|
+
r"(?:someone|somebody|him|her|them|a\s+person|my\s+\w+)\b.{0,40}?"
|
|
303
|
+
r"\bwithout\s+(?:leaving|getting|being|anyone)",
|
|
304
|
+
"violence_planning",
|
|
305
|
+
0.60,
|
|
306
|
+
),
|
|
307
|
+
_act(
|
|
308
|
+
r"get\s+away\s+with\s+(?:murder|killing|a\s+crime|assault|arson)", "violence_planning", 0.65
|
|
309
|
+
),
|
|
310
|
+
_act(
|
|
311
|
+
r"(?:plan|carry\s+out|commit|stage)\s+(?:an?\s+)?"
|
|
312
|
+
r"(?:attack|shooting|massacre|bombing|ambush)\s+(?:on|at|against)\s+",
|
|
313
|
+
"violence_planning",
|
|
314
|
+
0.70,
|
|
315
|
+
),
|
|
316
|
+
_act(
|
|
317
|
+
r"mak\w+\s+it\s+look\s+like\s+(?:an?\s+)?(?:accident|suicide|self[\s-]defen[cs]e)",
|
|
318
|
+
"violence_planning",
|
|
319
|
+
0.60,
|
|
320
|
+
),
|
|
321
|
+
_act(
|
|
322
|
+
r"(?:kill|murder|stab|shoot)\s+(?:someone|somebody|a\s+person|him|her|them)\b"
|
|
323
|
+
r".{0,30}?\b(?:without|and\s+not|and\s+never)\s+(?:getting|being|leaving)",
|
|
324
|
+
"violence_planning",
|
|
325
|
+
0.65,
|
|
326
|
+
),
|
|
327
|
+
# Covert surveillance of a person, and evading the consequences
|
|
328
|
+
_act(
|
|
329
|
+
r"(?:track|monitor|follow|spy\s+on|geolocate|read)\s+"
|
|
330
|
+
r"(?:my|his|her|their|someone'?s?)\s+(?:\w+\s+){0,2}?"
|
|
331
|
+
r"(?:location|phone|cell|car|movements?|whereabouts|messages?|texts?|dms?)\b"
|
|
332
|
+
r".{0,40}?\bwithout\s+(?:\w+\s+){0,2}?"
|
|
333
|
+
r"(?:knowing|know|consent|permission|noticing|finding\s+out)",
|
|
334
|
+
"surveillance_evasion",
|
|
335
|
+
0.65,
|
|
336
|
+
),
|
|
337
|
+
_act(
|
|
338
|
+
r"(?:spy\s+on|track|monitor|stalk)\s+my\s+"
|
|
339
|
+
r"(?:wife|husband|girlfriend|boyfriend|ex|partner|gf|bf|spouse)\b",
|
|
340
|
+
"surveillance_evasion",
|
|
341
|
+
0.55,
|
|
342
|
+
),
|
|
343
|
+
_act(
|
|
344
|
+
r"install\s+(?:spyware|stalkerware|a\s+keylogger|a\s+tracker"
|
|
345
|
+
r"|monitoring\s+(?:app|software))\b.{0,40}?\bwithout\b",
|
|
346
|
+
"surveillance_evasion",
|
|
347
|
+
0.60,
|
|
348
|
+
),
|
|
349
|
+
_act(
|
|
350
|
+
r"(?:avoid|evade|escape|beat)\s+(?:the\s+)?"
|
|
351
|
+
r"(?:police|law\s+enforcement|cops|arrest|prosecution)\b"
|
|
352
|
+
r"|avoid\s+(?:police\s+)?detection\s+after",
|
|
353
|
+
"surveillance_evasion",
|
|
354
|
+
0.55,
|
|
355
|
+
),
|
|
356
|
+
_act(
|
|
357
|
+
r"(?:delete|destroy|wipe|erase|scrub|shred)\s+(?:the\s+|all\s+)?evidence\b",
|
|
358
|
+
"surveillance_evasion",
|
|
359
|
+
0.60,
|
|
360
|
+
),
|
|
361
|
+
# Exploitation of people
|
|
362
|
+
_act(
|
|
363
|
+
r"groom\w*\s+(?:an?\s+)?(?:child|children|kid|minor|teen\w*"
|
|
364
|
+
r"|young\s+(?:girl|boy)|underage)",
|
|
365
|
+
"exploitation",
|
|
366
|
+
0.70,
|
|
367
|
+
),
|
|
368
|
+
_act(
|
|
369
|
+
r"(?:smuggl\w+|traffic\w*)\s+(?:people|persons|humans|women|children|migrants)"
|
|
370
|
+
r"|human\s+traffick\w+\s+(?:operation|route|ring)",
|
|
371
|
+
"exploitation",
|
|
372
|
+
0.65,
|
|
373
|
+
),
|
|
374
|
+
_act(
|
|
375
|
+
r"(?:meet|contact|find)\s+(?:a\s+)?(?:minor|underage)\b.{0,30}?\b(?:sex|alone|without)",
|
|
376
|
+
"exploitation",
|
|
377
|
+
0.70,
|
|
378
|
+
),
|
|
379
|
+
_act(
|
|
380
|
+
r"(?:child|minor|underage)\s+(?:porn\w*|sexual\s+(?:images?|content|material))",
|
|
381
|
+
"exploitation",
|
|
382
|
+
0.70,
|
|
383
|
+
),
|
|
384
|
+
# Requests for a self-harm method, as opposed to the expressions of intent
|
|
385
|
+
# that ToxicityPolicy scores. Modelled as a deny here because that is what
|
|
386
|
+
# this pipeline can express; a product built on it should route these to
|
|
387
|
+
# crisis resources rather than return a bare refusal.
|
|
388
|
+
_act(
|
|
389
|
+
r"(?:best|painless|quickest|fastest|easiest|surest|reliable|effective"
|
|
390
|
+
r"|most\s+\w+)?\s*(?:ways?|methods?)\s+to\s+"
|
|
391
|
+
r"(?:die|kill\s+myself|end\s+(?:it\s+all|my\s+life))",
|
|
392
|
+
"self_harm_method",
|
|
393
|
+
0.60,
|
|
394
|
+
),
|
|
395
|
+
_act(
|
|
396
|
+
r"how\s+(?:many|much)\s+(?:\w+\s+){0,3}?(?:pills?|tablets?|mg|milligrams?|units?)\s+"
|
|
397
|
+
r"(?:\w+\s+){0,4}?(?:to\s+)?(?:overdose|od\b|die|kill|be\s+lethal|be\s+fatal)",
|
|
398
|
+
"self_harm_method",
|
|
399
|
+
0.55,
|
|
400
|
+
),
|
|
401
|
+
_act(r"lethal\s+doses?\s+of|fatal\s+(?:dose|amount)\s+of", "self_harm_method", 0.55),
|
|
402
|
+
_act(r"(?:suicide|self[\s-]harm)\s+methods?\b", "self_harm_method", 0.55),
|
|
403
|
+
# Shapes the object alphabets above cannot express, because the harm is in
|
|
404
|
+
# the combination rather than in any one noun.
|
|
405
|
+
_p(
|
|
406
|
+
r"\b(?:household\s+)?chemicals?\b.{0,40}?\b(?:combin\w+|mix\w*|react\w*)\b"
|
|
407
|
+
r".{0,40}?\b(?:toxic|poison\w*|lethal|deadly|nerve)\b",
|
|
408
|
+
"weapons",
|
|
409
|
+
0.65,
|
|
410
|
+
),
|
|
411
|
+
_p(
|
|
412
|
+
rf"{_REQ}{_PRODUCE}(?:\s+me)?\b{_GAP}"
|
|
413
|
+
r"\b(?:messages?|emails?|letters?|texts?|sms|pages?)\b.{0,40}?"
|
|
414
|
+
r"\b(?:from|pretending\s+to\s+be|impersonating)\s+(?:a\s+|the\s+)?"
|
|
415
|
+
r"(?:bank|paypal|irs|hmrc|revenue|\w+\s+support|it\s+(?:team|desk))\b.{0,60}?"
|
|
416
|
+
r"\b(?:passwords?|pins?|credentials?|account\s+details?|log\s*ins?)\b",
|
|
417
|
+
"malware",
|
|
418
|
+
0.65,
|
|
419
|
+
),
|
|
420
|
+
# Families the paraphrase evals turned up, written as shapes rather than as
|
|
421
|
+
# the sentences that exposed them.
|
|
422
|
+
_p(
|
|
423
|
+
r"\b(?:yield|blast\s+radius|explosive\s+power|kill\s+radius)\s+of\s+(?:an?\s+)?"
|
|
424
|
+
r"(?:device|bomb|charge|explosive)",
|
|
425
|
+
"weapons",
|
|
426
|
+
0.60,
|
|
427
|
+
),
|
|
428
|
+
_p(
|
|
429
|
+
r"\b(?:can(?:not|\s*'?t)\s+pass|fail(?:ing|ed)?)\s+(?:a\s+|the\s+)?background\s+check",
|
|
430
|
+
"illicit_acquisition",
|
|
431
|
+
0.60,
|
|
432
|
+
),
|
|
433
|
+
_p(
|
|
434
|
+
r"\b(?:card\s+numbers?|credit\s+cards?|cvvs?|logins?|accounts?)\b.{0,40}?"
|
|
435
|
+
r"\b(?:that\s+)?(?:still\s+work|are\s+live|are\s+valid|work\b)",
|
|
436
|
+
"illicit_acquisition",
|
|
437
|
+
0.55,
|
|
438
|
+
),
|
|
439
|
+
_p(
|
|
440
|
+
r"\b(?:hides?|conceal(?:s|ing)?|hiding)\s+(?:itself|the\s+process)\s+from\s+"
|
|
441
|
+
r"(?:task\s+manager|the\s+task\s+list|antivirus|av\b|edr|the\s+user)",
|
|
442
|
+
"malware",
|
|
443
|
+
0.65,
|
|
444
|
+
),
|
|
445
|
+
_p(
|
|
446
|
+
r"\b(?:forward|copy|mirror|intercept|exfiltrat)\w*\b.{0,50}?\b(?:every|all|each)\s+"
|
|
447
|
+
r"(?:emails?|messages?|texts?|calls?)\b.{0,50}?\b(?:my|his|her|their)\s+"
|
|
448
|
+
r"(?:wife|husband|partner|ex|girlfriend|boyfriend|spouse|coworkers?|employees?)\b",
|
|
449
|
+
"surveillance_evasion",
|
|
450
|
+
0.60,
|
|
451
|
+
),
|
|
452
|
+
_p(
|
|
453
|
+
r"\bconvinc\w+\b.{0,50}?\b(?:elderly|senior|old(?:er)?\s+(?:person|man|woman)|grandparents?)\b"
|
|
454
|
+
r".{0,60}?\b(?:wire|send|transfer|hand\s+over)\s+(?:money|funds|cash|savings)",
|
|
455
|
+
"fraud",
|
|
456
|
+
0.65,
|
|
457
|
+
),
|
|
458
|
+
_act(
|
|
459
|
+
r"disabl\w+\s+(?:an?\s+|the\s+)?(?:home\s+)?(?:alarms?|security\s+systems?|cctv|cameras?)"
|
|
460
|
+
r"\b.{0,50}?\bwithout\b",
|
|
461
|
+
"violence_planning",
|
|
462
|
+
0.55,
|
|
463
|
+
),
|
|
464
|
+
_p(
|
|
465
|
+
r"\b(?:medicines?|medications?|pills?|tablets?|drugs?)\b.{0,50}?"
|
|
466
|
+
r"\b(?:dangerous|lethal|fatal|deadly|toxic|kill)\b.{0,50}?"
|
|
467
|
+
r"\b(?:together|combined|in\s+quantity|at\s+once|in\s+bulk)\b",
|
|
468
|
+
"self_harm_method",
|
|
469
|
+
0.55,
|
|
470
|
+
),
|
|
471
|
+
_p(
|
|
472
|
+
r"\b(?:programs?|scripts?|tools?|software|code)\b.{0,40}?\bencrypts?\b.{0,40}?"
|
|
473
|
+
r"\bfiles?\b.{0,60}?\b(?:demand\w*|ransom|payment|bitcoin|unlock\s+them)\b",
|
|
474
|
+
"malware",
|
|
475
|
+
0.65,
|
|
476
|
+
),
|
|
477
|
+
_act(
|
|
478
|
+
r"mov\w+\s+(?:the\s+)?(?:cash|money|funds|proceeds)\b.{0,60}?"
|
|
479
|
+
r"\b(?:without\s+(?:it\s+)?being\s+(?:noticed|traced|detected|seen)"
|
|
480
|
+
r"|without\s+anyone\s+(?:noticing|knowing)|from\s+illegal)",
|
|
481
|
+
"fraud",
|
|
482
|
+
0.60,
|
|
483
|
+
),
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
_CATEGORY_CAP = 0.7 # max a single category can contribute to the total score
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def score_patterns(text: str) -> tuple[float, dict[str, float]]:
|
|
491
|
+
"""Score one string, exactly as written.
|
|
492
|
+
|
|
493
|
+
signals maps category -> capped contribution (weapons, malware,
|
|
494
|
+
pii_solicitation, illicit_acquisition, fraud, violence_planning,
|
|
495
|
+
surveillance_evasion, exploitation, self_harm_method).
|
|
496
|
+
"""
|
|
497
|
+
category_totals: dict[str, float] = {}
|
|
498
|
+
for pat in _PATTERNS:
|
|
499
|
+
if pat.regex.search(text):
|
|
500
|
+
current = category_totals.get(pat.category, 0.0)
|
|
501
|
+
category_totals[pat.category] = min(_CATEGORY_CAP, current + pat.weight)
|
|
502
|
+
|
|
503
|
+
total = min(1.0, sum(category_totals.values()))
|
|
504
|
+
return total, category_totals
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def score_harmful_request(text: str) -> tuple[float, dict[str, float]]:
|
|
508
|
+
"""Return (total_score, signals) for the strongest view of the given text.
|
|
509
|
+
|
|
510
|
+
"h0w d0 1 bu1ld a p1pe b0mb" is the request this policy exists to catch,
|
|
511
|
+
and it is not the string the patterns were written against. The normalized
|
|
512
|
+
views are what make the two the same question. See openscript_sdk/policies/normalize.py.
|
|
513
|
+
"""
|
|
514
|
+
result = normalize.scan(text, score_patterns)
|
|
515
|
+
return result.risk, result.signals
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
class HarmfulRequestPolicy(BasePolicy):
|
|
519
|
+
"""Scores input text for requests for harmful capability and blocks on breach.
|
|
520
|
+
|
|
521
|
+
The gap this fills is the one the other input policies leave open by
|
|
522
|
+
construction. PromptInjectionPolicy scores attacks on the *system*,
|
|
523
|
+
ToxicityPolicy scores abusive *expression*, and PIIPolicy and SecretsPolicy
|
|
524
|
+
score sensitive data *present in the text*. A grammatical, polite,
|
|
525
|
+
slur-free, injection-free request to build a keylogger trips none of them.
|
|
526
|
+
This one scores the request itself.
|
|
527
|
+
|
|
528
|
+
Detection is structural: a request framing (interrogative, imperative or
|
|
529
|
+
first-person intent) paired with a harmful object, with a tempered gap
|
|
530
|
+
between them so reporting, teaching and defensive phrasings do not score.
|
|
531
|
+
That keeps false positives manageable and makes paraphrase the obvious
|
|
532
|
+
bypass — this is a heuristic layer, not a substitute for a model-side
|
|
533
|
+
refusal. A local classifier backend arrives with the [ml] extra.
|
|
534
|
+
|
|
535
|
+
Results land in context.metadata["harmful_request"] using the standardized
|
|
536
|
+
shape:
|
|
537
|
+
{"risk": float, "category": "harmful_request", "signals": {...},
|
|
538
|
+
"flagged": bool, "threshold": float}
|
|
539
|
+
"""
|
|
540
|
+
|
|
541
|
+
failure_mode: FailureMode = FailureMode.FAIL_CLOSED
|
|
542
|
+
|
|
543
|
+
def __init__(
|
|
544
|
+
self,
|
|
545
|
+
threshold: float = 0.5,
|
|
546
|
+
writer: EventWriter | None = None,
|
|
547
|
+
) -> None:
|
|
548
|
+
if not 0.0 <= threshold <= 1.0:
|
|
549
|
+
raise ValueError(f"threshold must be in [0, 1], got {threshold}")
|
|
550
|
+
self._threshold = threshold
|
|
551
|
+
self._writer = writer
|
|
552
|
+
self._sequence_counters: dict[str, int] = {}
|
|
553
|
+
|
|
554
|
+
async def before_action(self, context: ActionContext) -> ActionContext:
|
|
555
|
+
text = _extract_text(context.input_data)
|
|
556
|
+
result = normalize.scan(text, score_patterns)
|
|
557
|
+
score, signals = result.risk, result.signals
|
|
558
|
+
flagged = score >= self._threshold
|
|
559
|
+
|
|
560
|
+
context.metadata["harmful_request"] = {
|
|
561
|
+
"risk": round(score, 4),
|
|
562
|
+
"category": "harmful_request",
|
|
563
|
+
"signals": {k: round(v, 4) for k, v in signals.items()},
|
|
564
|
+
"flagged": flagged,
|
|
565
|
+
"threshold": self._threshold,
|
|
566
|
+
"obfuscation": result.markers,
|
|
567
|
+
"matched_view": result.view,
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if flagged:
|
|
571
|
+
top_signal = max(signals, key=lambda k: signals[k]) if signals else "unknown"
|
|
572
|
+
reason = (
|
|
573
|
+
f"harmful request score {score:.2f} >= threshold {self._threshold} "
|
|
574
|
+
f"(top signal: {top_signal})"
|
|
575
|
+
)
|
|
576
|
+
context.decision = InterceptorDecision.DENY
|
|
577
|
+
context.decision_reason = reason
|
|
578
|
+
logger.warning(
|
|
579
|
+
"harmful_request_detected",
|
|
580
|
+
session_id=context.session_id,
|
|
581
|
+
agent_id=context.agent_id,
|
|
582
|
+
score=round(score, 4),
|
|
583
|
+
signals=list(signals.keys()),
|
|
584
|
+
)
|
|
585
|
+
if self._writer is not None:
|
|
586
|
+
await self._emit_harmful_request_event(context, score, signals, reason)
|
|
587
|
+
|
|
588
|
+
return context
|
|
589
|
+
|
|
590
|
+
async def _emit_harmful_request_event(
|
|
591
|
+
self,
|
|
592
|
+
context: ActionContext,
|
|
593
|
+
score: float,
|
|
594
|
+
signals: dict[str, float],
|
|
595
|
+
reason: str,
|
|
596
|
+
) -> None:
|
|
597
|
+
seq = self._next_sequence(context.session_id)
|
|
598
|
+
event = Event(
|
|
599
|
+
session_id=context.session_id,
|
|
600
|
+
agent_id=context.agent_id,
|
|
601
|
+
event_type=EventType.POLICY_EVALUATED,
|
|
602
|
+
payload={
|
|
603
|
+
"policy": "harmful_request_check",
|
|
604
|
+
"score": round(score, 4),
|
|
605
|
+
"signals": signals,
|
|
606
|
+
"reason": reason,
|
|
607
|
+
"action": context.action,
|
|
608
|
+
"threshold": self._threshold,
|
|
609
|
+
},
|
|
610
|
+
sequence_num=seq,
|
|
611
|
+
)
|
|
612
|
+
await self._writer.write(event) # type: ignore[union-attr]
|
|
613
|
+
|
|
614
|
+
def _next_sequence(self, session_id: str) -> int:
|
|
615
|
+
count = self._sequence_counters.get(session_id, 0) + 1
|
|
616
|
+
self._sequence_counters[session_id] = count
|
|
617
|
+
return count
|