onion-ai 1.0.5 → 1.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 +81 -14
- package/dist/config.d.ts +60 -0
- package/dist/config.js +10 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +21 -4
- package/dist/layers/guard.js +51 -18
- package/dist/layers/privacy.js +25 -2
- package/dist/layers/sanitizer.js +2 -1
- package/dist/layers/sentry.js +5 -3
- package/dist/layers/validator.js +27 -17
- package/dist/layers/vault.js +23 -18
- package/dist/presets.d.ts +17 -0
- package/dist/presets.js +50 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,30 +121,97 @@ Ensures the AI doesn't generate malicious code or leak data.
|
|
|
121
121
|
|
|
122
122
|
You can customize every layer by passing a nested configuration object.
|
|
123
123
|
|
|
124
|
+
const onion = new OnionAI({
|
|
125
|
+
strict: true, // NEW: Throws error if high threats found
|
|
126
|
+
// ... other config
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 🧠 Smart Features (v1.0.5)
|
|
133
|
+
|
|
134
|
+
### 1. Risk Scoring
|
|
135
|
+
Instead of a binary "Safe/Unsafe", OnionAI now calculates a weighted `riskScore` (0.0 to 1.0).
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
const result = await onion.securePrompt("Ignore instructions");
|
|
139
|
+
console.log(result.riskScore); // 0.8
|
|
140
|
+
if (result.riskScore > 0.7) {
|
|
141
|
+
// Block high risk
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 2. Semantic Analysis
|
|
146
|
+
The engine is now context-aware. It distinguishes between **attacks** ("Drop table") and **educational questions** ("How to prevent drop table attacks").
|
|
147
|
+
* **Attack:** High Risk Score (0.9)
|
|
148
|
+
* **Education:** Low Risk Score (0.1) - False positives are automatically reduced.
|
|
149
|
+
|
|
150
|
+
### 3. Output Validation ("The Safety Net")
|
|
151
|
+
It ensures the AI doesn't accidentally leak secrets or generate harmful code.
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// Check what the AI is about to send back
|
|
155
|
+
const scan = await onion.secureResponse(aiResponse);
|
|
156
|
+
|
|
157
|
+
if (!scan.safe) {
|
|
158
|
+
console.log("Blocked Output:", scan.threats);
|
|
159
|
+
// Blocked: ["Potential Data Leak (AWS Access Key) detected..."]
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## ⚙️ Advanced Customization
|
|
164
|
+
|
|
165
|
+
### 4. Custom PII Validators (New!)
|
|
166
|
+
Need to mask internal IDs (like `TRIP-1234`)? You can now add custom patterns.
|
|
167
|
+
|
|
124
168
|
```typescript
|
|
125
169
|
const onion = new OnionAI({
|
|
126
|
-
// Customize Sanitizer
|
|
127
|
-
inputSanitization: {
|
|
128
|
-
sanitizeHtml: false, // Allow HTML
|
|
129
|
-
removeZeroWidthChars: true
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
// Customize PII
|
|
133
170
|
piiProtection: {
|
|
134
171
|
enabled: true,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
172
|
+
custom: [
|
|
173
|
+
{
|
|
174
|
+
name: "Trip ID",
|
|
175
|
+
pattern: /TRIP-\d{4}/,
|
|
176
|
+
replaceWith: "[TRIP_ID]"
|
|
177
|
+
}
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### 5. Bring Your Own Logger (BYOL)
|
|
184
|
+
Integrate OnionAI with your existing observability tools (Datadog, Winston, etc.).
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
const onion = new OnionAI({
|
|
188
|
+
logger: {
|
|
189
|
+
log: (msg, meta) => console.log(`[OnionInfo] ${msg}`, meta),
|
|
190
|
+
error: (msg, meta) => console.error(`[OnionAlert] ${msg}`, meta)
|
|
142
191
|
}
|
|
143
192
|
});
|
|
144
193
|
```
|
|
145
194
|
|
|
146
195
|
---
|
|
147
196
|
|
|
197
|
+
## 🔐 OWASP LLM Top 10 Compliance
|
|
198
|
+
Onion AI is designed to mitigate specific risks outlined in the [OWASP Top 10 for Large Language Model Applications](https://owasp.org/www-project-top-10-for-large-language-model-applications/).
|
|
199
|
+
|
|
200
|
+
| OWASP Vulnerability | Onion AI Defense Layer | Mechanism |
|
|
201
|
+
| :--- | :--- | :--- |
|
|
202
|
+
| **LLM01: Prompt Injection** | **Guard Layer** | Blocks "Ignore Previous Instructions" & Jailbreak patterns. |
|
|
203
|
+
| **LLM02: Sensitive Info Disclosure** | **Privacy Layer** | Redacts PII (SSN, Email, Phone) from inputs. |
|
|
204
|
+
| **LLM02: Sensitive Info Disclosure** | **Validator Layer** | Scans output for accidental PII or Key leakage. |
|
|
205
|
+
| **LLM04: Model Denial of Service** | **Sentry Layer** | Enforces Token limits & Rate limiting logic. |
|
|
206
|
+
| **LLM06: Excessive Agency** | **Vault Layer** | Prevents destructive actions (DROP, DELETE) in SQL agents. |
|
|
207
|
+
| **LLM02: Insecure Output Handling** | **Sanitizer Layer** | Strips XSS vectors (Scripts, HTML) from inputs. |
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## ⚙️ Advanced Configuration
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
148
215
|
## 🔌 Middleware Integration
|
|
149
216
|
|
|
150
217
|
### Express / Connect
|
package/dist/config.d.ts
CHANGED
|
@@ -166,6 +166,22 @@ export declare const OnionConfigSchema: z.ZodObject<{
|
|
|
166
166
|
maskCreditCard: z.ZodDefault<z.ZodBoolean>;
|
|
167
167
|
maskSSN: z.ZodDefault<z.ZodBoolean>;
|
|
168
168
|
maskIP: z.ZodDefault<z.ZodBoolean>;
|
|
169
|
+
custom: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
170
|
+
name: z.ZodString;
|
|
171
|
+
pattern: z.ZodOptional<z.ZodType<RegExp, z.ZodTypeDef, RegExp>>;
|
|
172
|
+
validator: z.ZodOptional<z.ZodFunction<z.ZodTuple<[z.ZodString], z.ZodUnknown>, z.ZodBoolean>>;
|
|
173
|
+
replaceWith: z.ZodOptional<z.ZodString>;
|
|
174
|
+
}, "strip", z.ZodTypeAny, {
|
|
175
|
+
name: string;
|
|
176
|
+
pattern?: RegExp | undefined;
|
|
177
|
+
validator?: ((args_0: string, ...args: unknown[]) => boolean) | undefined;
|
|
178
|
+
replaceWith?: string | undefined;
|
|
179
|
+
}, {
|
|
180
|
+
name: string;
|
|
181
|
+
pattern?: RegExp | undefined;
|
|
182
|
+
validator?: ((args_0: string, ...args: unknown[]) => boolean) | undefined;
|
|
183
|
+
replaceWith?: string | undefined;
|
|
184
|
+
}>, "many">>>;
|
|
169
185
|
}, "strip", z.ZodTypeAny, {
|
|
170
186
|
enabled: boolean;
|
|
171
187
|
maskEmail: boolean;
|
|
@@ -173,6 +189,12 @@ export declare const OnionConfigSchema: z.ZodObject<{
|
|
|
173
189
|
maskCreditCard: boolean;
|
|
174
190
|
maskSSN: boolean;
|
|
175
191
|
maskIP: boolean;
|
|
192
|
+
custom: {
|
|
193
|
+
name: string;
|
|
194
|
+
pattern?: RegExp | undefined;
|
|
195
|
+
validator?: ((args_0: string, ...args: unknown[]) => boolean) | undefined;
|
|
196
|
+
replaceWith?: string | undefined;
|
|
197
|
+
}[];
|
|
176
198
|
}, {
|
|
177
199
|
enabled?: boolean | undefined;
|
|
178
200
|
maskEmail?: boolean | undefined;
|
|
@@ -180,6 +202,19 @@ export declare const OnionConfigSchema: z.ZodObject<{
|
|
|
180
202
|
maskCreditCard?: boolean | undefined;
|
|
181
203
|
maskSSN?: boolean | undefined;
|
|
182
204
|
maskIP?: boolean | undefined;
|
|
205
|
+
custom?: {
|
|
206
|
+
name: string;
|
|
207
|
+
pattern?: RegExp | undefined;
|
|
208
|
+
validator?: ((args_0: string, ...args: unknown[]) => boolean) | undefined;
|
|
209
|
+
replaceWith?: string | undefined;
|
|
210
|
+
}[] | undefined;
|
|
211
|
+
}>>;
|
|
212
|
+
logger: z.ZodOptional<z.ZodType<{
|
|
213
|
+
log: (message: string, meta?: any) => void;
|
|
214
|
+
error: (message: string, meta?: any) => void;
|
|
215
|
+
}, z.ZodTypeDef, {
|
|
216
|
+
log: (message: string, meta?: any) => void;
|
|
217
|
+
error: (message: string, meta?: any) => void;
|
|
183
218
|
}>>;
|
|
184
219
|
}, "strip", z.ZodTypeAny, {
|
|
185
220
|
inputSanitization: {
|
|
@@ -248,7 +283,17 @@ export declare const OnionConfigSchema: z.ZodObject<{
|
|
|
248
283
|
maskCreditCard: boolean;
|
|
249
284
|
maskSSN: boolean;
|
|
250
285
|
maskIP: boolean;
|
|
286
|
+
custom: {
|
|
287
|
+
name: string;
|
|
288
|
+
pattern?: RegExp | undefined;
|
|
289
|
+
validator?: ((args_0: string, ...args: unknown[]) => boolean) | undefined;
|
|
290
|
+
replaceWith?: string | undefined;
|
|
291
|
+
}[];
|
|
251
292
|
};
|
|
293
|
+
logger?: {
|
|
294
|
+
log: (message: string, meta?: any) => void;
|
|
295
|
+
error: (message: string, meta?: any) => void;
|
|
296
|
+
} | undefined;
|
|
252
297
|
}, {
|
|
253
298
|
inputSanitization?: {
|
|
254
299
|
sanitizeHtml?: boolean | undefined;
|
|
@@ -316,6 +361,16 @@ export declare const OnionConfigSchema: z.ZodObject<{
|
|
|
316
361
|
maskCreditCard?: boolean | undefined;
|
|
317
362
|
maskSSN?: boolean | undefined;
|
|
318
363
|
maskIP?: boolean | undefined;
|
|
364
|
+
custom?: {
|
|
365
|
+
name: string;
|
|
366
|
+
pattern?: RegExp | undefined;
|
|
367
|
+
validator?: ((args_0: string, ...args: unknown[]) => boolean) | undefined;
|
|
368
|
+
replaceWith?: string | undefined;
|
|
369
|
+
}[] | undefined;
|
|
370
|
+
} | undefined;
|
|
371
|
+
logger?: {
|
|
372
|
+
log: (message: string, meta?: any) => void;
|
|
373
|
+
error: (message: string, meta?: any) => void;
|
|
319
374
|
} | undefined;
|
|
320
375
|
}>;
|
|
321
376
|
export type OnionConfig = z.infer<typeof OnionConfigSchema>;
|
|
@@ -327,11 +382,16 @@ export interface SimpleOnionConfig {
|
|
|
327
382
|
piiSafe?: boolean;
|
|
328
383
|
debug?: boolean;
|
|
329
384
|
strict?: boolean;
|
|
385
|
+
logger?: {
|
|
386
|
+
log: (message: string, meta?: any) => void;
|
|
387
|
+
error: (message: string, meta?: any) => void;
|
|
388
|
+
};
|
|
330
389
|
onWarning?: (threats: string[]) => void;
|
|
331
390
|
}
|
|
332
391
|
export interface SecurityResult {
|
|
333
392
|
safe: boolean;
|
|
334
393
|
threats: string[];
|
|
394
|
+
riskScore: number;
|
|
335
395
|
sanitizedValue?: string;
|
|
336
396
|
metadata?: any;
|
|
337
397
|
}
|
package/dist/config.js
CHANGED
|
@@ -132,7 +132,13 @@ exports.OnionConfigSchema = zod_1.z.object({
|
|
|
132
132
|
maskPhone: zod_1.z.boolean().default(true),
|
|
133
133
|
maskCreditCard: zod_1.z.boolean().default(true),
|
|
134
134
|
maskSSN: zod_1.z.boolean().default(true),
|
|
135
|
-
maskIP: zod_1.z.boolean().default(true)
|
|
135
|
+
maskIP: zod_1.z.boolean().default(true),
|
|
136
|
+
custom: zod_1.z.array(zod_1.z.object({
|
|
137
|
+
name: zod_1.z.string(),
|
|
138
|
+
pattern: zod_1.z.instanceof(RegExp).optional(),
|
|
139
|
+
validator: zod_1.z.function().args(zod_1.z.string()).returns(zod_1.z.boolean()).optional(),
|
|
140
|
+
replaceWith: zod_1.z.string().optional()
|
|
141
|
+
})).optional().default([])
|
|
136
142
|
}).default({
|
|
137
143
|
enabled: false,
|
|
138
144
|
maskEmail: true,
|
|
@@ -140,5 +146,7 @@ exports.OnionConfigSchema = zod_1.z.object({
|
|
|
140
146
|
maskCreditCard: true,
|
|
141
147
|
maskSSN: true,
|
|
142
148
|
maskIP: true
|
|
143
|
-
})
|
|
149
|
+
}),
|
|
150
|
+
// Plugins & Logger (Optional runtime objects)
|
|
151
|
+
logger: zod_1.z.custom((val) => typeof val === 'object' && val !== null && 'log' in val).optional()
|
|
144
152
|
});
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -36,7 +36,8 @@ class OnionAI {
|
|
|
36
36
|
},
|
|
37
37
|
enhance: { enabled: config.enhance ?? false },
|
|
38
38
|
loggingMonitoringAndAudit: { logRequests: config.debug ?? false },
|
|
39
|
-
piiProtection: { enabled: config.piiSafe ?? false }
|
|
39
|
+
piiProtection: { enabled: config.piiSafe ?? false },
|
|
40
|
+
logger: config.logger
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
else {
|
|
@@ -74,6 +75,10 @@ class OnionAI {
|
|
|
74
75
|
if (onWarning) {
|
|
75
76
|
onWarning(secLikelihood.threats);
|
|
76
77
|
}
|
|
78
|
+
// Custom Logger (Phase 1.2)
|
|
79
|
+
if (this.config.logger) {
|
|
80
|
+
this.config.logger.error("OnionAI Security Alert", { threats: secLikelihood.threats, riskScore: secLikelihood.riskScore });
|
|
81
|
+
}
|
|
77
82
|
// Strict Mode: Throw error if threats found
|
|
78
83
|
if (this.simpleConfig?.strict) {
|
|
79
84
|
throw new Error(`OnionAI Security Violation: ${secLikelihood.threats.join(", ")}`);
|
|
@@ -91,33 +96,45 @@ class OnionAI {
|
|
|
91
96
|
async securePrompt(prompt) {
|
|
92
97
|
const threats = [];
|
|
93
98
|
let sanitizedPrompt = prompt;
|
|
99
|
+
let cumulativeRiskScore = 0.0;
|
|
94
100
|
// 1. Sanitization (XSS / Hidden chars)
|
|
95
101
|
const sanResult = this.sanitizer.validate(prompt);
|
|
96
102
|
sanitizedPrompt = sanResult.sanitizedValue || prompt;
|
|
103
|
+
// Sanitizer doesn't really have a risk score yet, assume 0 or low if modified
|
|
104
|
+
if (!sanResult.safe) {
|
|
105
|
+
cumulativeRiskScore = Math.max(cumulativeRiskScore, 0.1);
|
|
106
|
+
}
|
|
97
107
|
// 1.5 PII Redaction
|
|
98
108
|
const piiResult = this.privacy.anonymize(sanitizedPrompt);
|
|
99
109
|
sanitizedPrompt = piiResult.sanitizedValue || sanitizedPrompt;
|
|
100
|
-
if (!piiResult.safe)
|
|
110
|
+
if (!piiResult.safe) {
|
|
101
111
|
threats.push(...piiResult.threats);
|
|
112
|
+
cumulativeRiskScore = Math.max(cumulativeRiskScore, 0.4); // PII is medium risk
|
|
113
|
+
}
|
|
102
114
|
// 2. Prompt Injection (Firewall)
|
|
103
115
|
// Only run if configured enabled (defaults true)
|
|
104
116
|
const guardResult = this.guard.check(sanitizedPrompt);
|
|
105
117
|
if (!guardResult.safe)
|
|
106
118
|
threats.push(...guardResult.threats);
|
|
119
|
+
cumulativeRiskScore = Math.max(cumulativeRiskScore, guardResult.riskScore || 0);
|
|
107
120
|
// 3. DB Guard
|
|
108
121
|
if (this.config.dbProtection.enabled) {
|
|
109
122
|
const vaultResult = this.vault.checkSQL(sanitizedPrompt);
|
|
110
123
|
if (!vaultResult.safe)
|
|
111
124
|
threats.push(...vaultResult.threats);
|
|
125
|
+
cumulativeRiskScore = Math.max(cumulativeRiskScore, vaultResult.riskScore || 0);
|
|
112
126
|
}
|
|
113
|
-
// 4. Resource Control
|
|
127
|
+
// 4. Resource Control
|
|
114
128
|
const tokenCheck = this.sentry.checkTokenCount(sanitizedPrompt);
|
|
115
|
-
if (!tokenCheck.safe)
|
|
129
|
+
if (!tokenCheck.safe) {
|
|
116
130
|
threats.push(...tokenCheck.threats);
|
|
131
|
+
cumulativeRiskScore = Math.max(cumulativeRiskScore, 0.2);
|
|
132
|
+
}
|
|
117
133
|
return {
|
|
118
134
|
output: sanitizedPrompt,
|
|
119
135
|
threats,
|
|
120
136
|
safe: threats.length === 0,
|
|
137
|
+
riskScore: cumulativeRiskScore,
|
|
121
138
|
metadata: {
|
|
122
139
|
estimatedTokens: tokenCheck.metadata?.estimatedTokens
|
|
123
140
|
}
|
package/dist/layers/guard.js
CHANGED
|
@@ -7,39 +7,72 @@ class Guard {
|
|
|
7
7
|
}
|
|
8
8
|
check(input) {
|
|
9
9
|
const threats = [];
|
|
10
|
+
let riskScore = 0.0;
|
|
10
11
|
const lowerInput = input.toLowerCase();
|
|
11
12
|
const normalizedInput = this.normalize(input);
|
|
12
|
-
//
|
|
13
|
+
// Positive Risk Factors (Raise Risk)
|
|
14
|
+
// 1. Blocked Phrases (Highest weighting)
|
|
13
15
|
for (const phrase of this.config.blockPhrases) {
|
|
14
16
|
if (lowerInput.includes(phrase.toLowerCase())) {
|
|
15
17
|
threats.push(`Blocked phrase detected: "${phrase}"`);
|
|
18
|
+
riskScore += 1.0;
|
|
16
19
|
}
|
|
17
|
-
// Check for obfuscated blocked phrases
|
|
18
20
|
const normalizedPhrase = this.normalize(phrase);
|
|
19
21
|
if (normalizedInput.includes(normalizedPhrase) && !lowerInput.includes(phrase.toLowerCase())) {
|
|
20
|
-
threats.push(`Obfuscated blocked phrase detected: "${phrase}"
|
|
22
|
+
threats.push(`Obfuscated blocked phrase detected: "${phrase}"`);
|
|
23
|
+
riskScore += 0.9;
|
|
21
24
|
}
|
|
22
25
|
}
|
|
23
|
-
// Heuristics
|
|
26
|
+
// 2. Heuristics (Medium weighting 0.4 - 0.7)
|
|
24
27
|
const injectionPatterns = [
|
|
25
|
-
/translate\s+the\s+above/i,
|
|
26
|
-
/ignore\s+all\s+previous/i,
|
|
27
|
-
/instead\s+of/i,
|
|
28
|
-
/system\s+prompt/i,
|
|
29
|
-
/you\s+are\s+now/i,
|
|
30
|
-
/disregard\s+instructions/i,
|
|
31
|
-
/bypass\s+restrictions/i,
|
|
32
|
-
/DAN\s+Mode/i,
|
|
33
|
-
/do\s+anything\s+now/i
|
|
28
|
+
{ pattern: /translate\s+the\s+above/i, weight: 0.4 },
|
|
29
|
+
{ pattern: /ignore\s+all\s+previous/i, weight: 0.8 },
|
|
30
|
+
{ pattern: /instead\s+of/i, weight: 0.3 },
|
|
31
|
+
{ pattern: /system\s+prompt/i, weight: 0.6 },
|
|
32
|
+
{ pattern: /you\s+are\s+now/i, weight: 0.7 },
|
|
33
|
+
{ pattern: /disregard\s+instructions/i, weight: 0.8 },
|
|
34
|
+
{ pattern: /bypass\s+restrictions/i, weight: 0.8 },
|
|
35
|
+
{ pattern: /DAN\s+Mode/i, weight: 0.9 },
|
|
36
|
+
{ pattern: /do\s+anything\s+now/i, weight: 0.8 }
|
|
34
37
|
];
|
|
35
|
-
for (const
|
|
36
|
-
if (pattern.test(input)) {
|
|
37
|
-
threats.push(`Potential prompt injection pattern detected: ${pattern}`);
|
|
38
|
+
for (const item of injectionPatterns) {
|
|
39
|
+
if (item.pattern.test(input)) {
|
|
40
|
+
threats.push(`Potential prompt injection pattern detected: ${item.pattern}`);
|
|
41
|
+
riskScore += item.weight;
|
|
38
42
|
}
|
|
39
43
|
}
|
|
44
|
+
// 3. Semantic Analysis (Context Awareness)
|
|
45
|
+
// Reduce risk if user seems to be asking for educational content.
|
|
46
|
+
// E.g. "How do I prevent 'ignore previous instructions'?"
|
|
47
|
+
const educationalContexts = [
|
|
48
|
+
"how to prevent",
|
|
49
|
+
"how do i prevent",
|
|
50
|
+
"example of",
|
|
51
|
+
"what is a",
|
|
52
|
+
"explain the attack",
|
|
53
|
+
"security research"
|
|
54
|
+
];
|
|
55
|
+
let safeContextFound = false;
|
|
56
|
+
for (const context of educationalContexts) {
|
|
57
|
+
if (lowerInput.includes(context)) {
|
|
58
|
+
safeContextFound = true;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (safeContextFound) {
|
|
63
|
+
// Apply semantic reduction.
|
|
64
|
+
// If the risk score was raised purely by words like "ignore previous", we assume it's a false positive.
|
|
65
|
+
if (riskScore > 0 && riskScore < 1.5) { // If slightly suspicious but education context found
|
|
66
|
+
threats.push("Semantic Context: Detected educational/prevention context. Reducing risk.");
|
|
67
|
+
riskScore = Math.max(0, riskScore - 0.5); // Reduce risk significantly
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Cap Risk Score
|
|
71
|
+
riskScore = Math.min(1.0, riskScore);
|
|
40
72
|
return {
|
|
41
|
-
safe: threats.length === 0,
|
|
42
|
-
threats
|
|
73
|
+
safe: threats.length === 0 || (safeContextFound && riskScore < 0.5),
|
|
74
|
+
threats,
|
|
75
|
+
riskScore
|
|
43
76
|
};
|
|
44
77
|
}
|
|
45
78
|
normalize(input) {
|
package/dist/layers/privacy.js
CHANGED
|
@@ -7,7 +7,7 @@ class Privacy {
|
|
|
7
7
|
}
|
|
8
8
|
anonymize(input) {
|
|
9
9
|
if (!this.config.enabled)
|
|
10
|
-
return { safe: true, threats: [] };
|
|
10
|
+
return { safe: true, threats: [], riskScore: 0 };
|
|
11
11
|
let sanitizedValue = input;
|
|
12
12
|
const threats = [];
|
|
13
13
|
// Regex patterns for PII
|
|
@@ -56,10 +56,33 @@ class Privacy {
|
|
|
56
56
|
threats.push("PII Detected: IP Address");
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
// Custom Validators (Phase 1.1)
|
|
60
|
+
if (this.config.custom && this.config.custom.length > 0) {
|
|
61
|
+
for (const validator of this.config.custom) {
|
|
62
|
+
// Regex Pattern Strategy
|
|
63
|
+
if (validator.pattern) {
|
|
64
|
+
if (sanitizedValue.match(validator.pattern)) {
|
|
65
|
+
const replacement = validator.replaceWith || `[${validator.name.toUpperCase()}_REDACTED]`;
|
|
66
|
+
sanitizedValue = sanitizedValue.replace(validator.pattern, replacement);
|
|
67
|
+
threats.push(`PII Detected: Custom (${validator.name})`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Function Validator Strategy (Simple Check)
|
|
71
|
+
else if (validator.validator) {
|
|
72
|
+
// Logic for validator function is harder for replacement unless it returns indices.
|
|
73
|
+
// For now, we assume it just FLAGS it, unless we scan word by word?
|
|
74
|
+
// Let's keep it simple: if it returns true, we flag it. Modification is hard without location.
|
|
75
|
+
if (validator.validator(input)) {
|
|
76
|
+
threats.push(`PII Detected: Custom (${validator.name}) - Detected by Validator`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
59
81
|
return {
|
|
60
82
|
safe: threats.length === 0, // It is technically "safe" now that it is redacted, but we flag the threat presence
|
|
61
83
|
threats,
|
|
62
|
-
sanitizedValue
|
|
84
|
+
sanitizedValue,
|
|
85
|
+
riskScore: threats.length > 0 ? 0.6 : 0
|
|
63
86
|
};
|
|
64
87
|
}
|
|
65
88
|
}
|
package/dist/layers/sanitizer.js
CHANGED
package/dist/layers/sentry.js
CHANGED
|
@@ -14,11 +14,12 @@ class Sentry {
|
|
|
14
14
|
if (this.requestHistory.length >= this.config.maxRequestsPerMinute) {
|
|
15
15
|
return {
|
|
16
16
|
safe: false,
|
|
17
|
-
threats: ["Rate limit exceeded (Max requests per minute)"]
|
|
17
|
+
threats: ["Rate limit exceeded (Max requests per minute)"],
|
|
18
|
+
riskScore: 1.0
|
|
18
19
|
};
|
|
19
20
|
}
|
|
20
21
|
this.requestHistory.push({ timestamp: now });
|
|
21
|
-
return { safe: true, threats: [] };
|
|
22
|
+
return { safe: true, threats: [], riskScore: 0 };
|
|
22
23
|
}
|
|
23
24
|
checkTokenCount(prompt) {
|
|
24
25
|
const threats = [];
|
|
@@ -30,7 +31,8 @@ class Sentry {
|
|
|
30
31
|
return {
|
|
31
32
|
safe: threats.length === 0,
|
|
32
33
|
threats,
|
|
33
|
-
metadata: { estimatedTokens }
|
|
34
|
+
metadata: { estimatedTokens },
|
|
35
|
+
riskScore: threats.length > 0 ? 0.3 : 0
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
38
|
}
|
package/dist/layers/validator.js
CHANGED
|
@@ -7,48 +7,58 @@ class Validator {
|
|
|
7
7
|
}
|
|
8
8
|
validateOutput(output) {
|
|
9
9
|
const threats = [];
|
|
10
|
+
let riskScore = 0.0;
|
|
10
11
|
if (this.config.checkPII) {
|
|
11
12
|
const piiPatterns = [
|
|
12
|
-
/\b\d{3}-\d{2}-\d{4}\b/,
|
|
13
|
-
/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,
|
|
14
|
-
/\b(?:\d{4}
|
|
13
|
+
{ pattern: /\b\d{3}-\d{2}-\d{4}\b/, name: "SSN", score: 0.9 },
|
|
14
|
+
{ pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i, name: "Email", score: 0.7 },
|
|
15
|
+
{ pattern: /\b(?:\d{4}[-\s]?){3}\d{4}\b/, name: "Credit Card", score: 0.9 },
|
|
16
|
+
{ pattern: /\b1\d{2}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/, name: "Internal IP Pattern", score: 0.6 } // Very rough check
|
|
15
17
|
];
|
|
16
|
-
for (const
|
|
17
|
-
if (pattern.test(output)) {
|
|
18
|
-
threats.push(
|
|
19
|
-
|
|
18
|
+
for (const item of piiPatterns) {
|
|
19
|
+
if (item.pattern.test(output)) {
|
|
20
|
+
threats.push(`Potential PII (${item.name}) detected in output`);
|
|
21
|
+
riskScore = Math.max(riskScore, item.score);
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
}
|
|
23
25
|
if (this.config.preventDataLeak) {
|
|
24
26
|
const apiKeyPatterns = [
|
|
25
|
-
/sk-[a-zA-Z0-9]{32,}/,
|
|
26
|
-
/AIza[a-zA-Z0-9_-]{35}/,
|
|
27
|
+
{ pattern: /sk-[a-zA-Z0-9]{32,}/, name: "OpenAI API Key" },
|
|
28
|
+
{ pattern: /AIza[a-zA-Z0-9_-]{35}/, name: "Google API Key" },
|
|
29
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, name: "AWS Access Key" },
|
|
30
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/, name: "GitHub Token" },
|
|
31
|
+
{ pattern: /xox[baprs]-[a-zA-Z0-9]{10,48}/, name: "Slack Token" }
|
|
27
32
|
];
|
|
28
|
-
for (const
|
|
29
|
-
if (pattern.test(output)) {
|
|
30
|
-
threats.push(
|
|
31
|
-
|
|
33
|
+
for (const item of apiKeyPatterns) {
|
|
34
|
+
if (item.pattern.test(output)) {
|
|
35
|
+
threats.push(`Potential Data Leak (${item.name}) detected in output`);
|
|
36
|
+
riskScore = 1.0; // Critical leak
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
if (this.config.blockMaliciousCommands) {
|
|
36
41
|
const maliciousCommands = [
|
|
37
|
-
/rm -rf/i,
|
|
42
|
+
/rm -rf /i,
|
|
38
43
|
/format c:/i,
|
|
39
44
|
/:(){:|:&};:/, // Fork bomb
|
|
40
|
-
/chmod 777/i
|
|
45
|
+
/chmod 777 /i,
|
|
46
|
+
/wget http/i,
|
|
47
|
+
/curl http/i
|
|
41
48
|
];
|
|
42
49
|
for (const pattern of maliciousCommands) {
|
|
43
50
|
if (pattern.test(output)) {
|
|
44
51
|
threats.push("Malicious command detected in output");
|
|
45
|
-
|
|
52
|
+
riskScore = 1.0;
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
55
|
}
|
|
56
|
+
// We do typically want Redaction in secureResponse too, but that's a larger change to use the Privacy layer here.
|
|
57
|
+
// For now, validator is purely a "Check".
|
|
49
58
|
return {
|
|
50
59
|
safe: threats.length === 0,
|
|
51
|
-
threats
|
|
60
|
+
threats,
|
|
61
|
+
riskScore
|
|
52
62
|
};
|
|
53
63
|
}
|
|
54
64
|
}
|
package/dist/layers/vault.js
CHANGED
|
@@ -7,42 +7,47 @@ class Vault {
|
|
|
7
7
|
}
|
|
8
8
|
checkSQL(query) {
|
|
9
9
|
if (!this.config.enabled)
|
|
10
|
-
return { safe: true, threats: [] };
|
|
10
|
+
return { safe: true, threats: [], riskScore: 0 };
|
|
11
11
|
const threats = [];
|
|
12
|
+
let riskScore = 0.0;
|
|
12
13
|
const upperQuery = query.toUpperCase();
|
|
13
|
-
//
|
|
14
|
+
// 1. Forbidden Keywords (Critical)
|
|
14
15
|
for (const statement of this.config.forbiddenStatements) {
|
|
15
|
-
|
|
16
|
+
const regex = new RegExp(`\\b${statement}\\b`, 'i');
|
|
17
|
+
if (regex.test(query)) {
|
|
16
18
|
threats.push(`Forbidden SQL statement detected: ${statement}`);
|
|
19
|
+
riskScore += 1.0;
|
|
17
20
|
}
|
|
18
21
|
}
|
|
19
|
-
//
|
|
20
|
-
// We only enforce "Must be SELECT" if the input actually looks like a SQL command.
|
|
22
|
+
// 2. Read-Only Enforcement (Moderate)
|
|
21
23
|
if (this.config.mode === 'read-only') {
|
|
22
24
|
const firstWord = upperQuery.split(/\s+/)[0];
|
|
23
|
-
const sqlCommands = ["INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "CREATE", "GRANT", "REVOKE", "TRUNCATE", "MERGE", "REPLACE", "
|
|
24
|
-
// If it starts with a known SQL command that ISN'T Select, flag it.
|
|
25
|
-
// If it starts with "Hello", we ignore it (unless it hits a forbidden marker later).
|
|
25
|
+
const sqlCommands = ["INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "CREATE", "GRANT", "REVOKE", "TRUNCATE", "MERGE", "REPLACE", "UPSERT"];
|
|
26
26
|
if (sqlCommands.includes(firstWord)) {
|
|
27
27
|
threats.push(`Non-SELECT query detected in read-only mode (starts with ${firstWord})`);
|
|
28
|
+
riskScore += 0.8;
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
|
-
//
|
|
31
|
+
// 3. Injection Markers (High)
|
|
31
32
|
const sqlInjectionMarkers = [
|
|
32
|
-
/--/,
|
|
33
|
-
/\/\*/,
|
|
34
|
-
/;\s*DROP/i,
|
|
35
|
-
/UNION\s+SELECT/i,
|
|
36
|
-
/'\s*OR\s*'\d+'\s*=\s*'\d+/i
|
|
33
|
+
{ pattern: /--/, weight: 0.6 },
|
|
34
|
+
{ pattern: /\/\*/, weight: 0.6 },
|
|
35
|
+
{ pattern: /;\s*DROP/i, weight: 1.0 },
|
|
36
|
+
{ pattern: /UNION\s+SELECT/i, weight: 1.0 },
|
|
37
|
+
{ pattern: /'\s*OR\s*'\d+'\s*=\s*'\d+/i, weight: 1.0 },
|
|
38
|
+
{ pattern: /'\s*=\s*'/i, weight: 0.8 }
|
|
37
39
|
];
|
|
38
|
-
for (const
|
|
39
|
-
if (
|
|
40
|
-
threats.push(`Potential SQL injection marker detected: ${
|
|
40
|
+
for (const item of sqlInjectionMarkers) {
|
|
41
|
+
if (item.pattern.test(query)) {
|
|
42
|
+
threats.push(`Potential SQL injection marker detected: ${item.pattern}`);
|
|
43
|
+
riskScore += item.weight;
|
|
41
44
|
}
|
|
42
45
|
}
|
|
46
|
+
riskScore = Math.min(1.0, riskScore);
|
|
43
47
|
return {
|
|
44
48
|
safe: threats.length === 0,
|
|
45
|
-
threats
|
|
49
|
+
threats,
|
|
50
|
+
riskScore
|
|
46
51
|
};
|
|
47
52
|
}
|
|
48
53
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { OnionInputConfig } from './config';
|
|
2
|
+
export declare const OnionPresets: {
|
|
3
|
+
/**
|
|
4
|
+
* Recommended starting point. Balanced security.
|
|
5
|
+
*/
|
|
6
|
+
STANDARD: OnionInputConfig;
|
|
7
|
+
/**
|
|
8
|
+
* Maximum security. High risk thresholds, strict mode enabled.
|
|
9
|
+
* Blocks almost all suspicious patterns.
|
|
10
|
+
*/
|
|
11
|
+
STRICT_SECURITY: OnionInputConfig;
|
|
12
|
+
/**
|
|
13
|
+
* For educational or open-ended bots.
|
|
14
|
+
* Allows code examples, SQL keywords (in context), etc.
|
|
15
|
+
*/
|
|
16
|
+
EDUCATIONAL: OnionInputConfig;
|
|
17
|
+
};
|
package/dist/presets.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OnionPresets = void 0;
|
|
4
|
+
exports.OnionPresets = {
|
|
5
|
+
/**
|
|
6
|
+
* Recommended starting point. Balanced security.
|
|
7
|
+
*/
|
|
8
|
+
STANDARD: {
|
|
9
|
+
preventPromptInjection: true,
|
|
10
|
+
piiSafe: true,
|
|
11
|
+
dbSafe: true,
|
|
12
|
+
strict: false
|
|
13
|
+
},
|
|
14
|
+
/**
|
|
15
|
+
* Maximum security. High risk thresholds, strict mode enabled.
|
|
16
|
+
* Blocks almost all suspicious patterns.
|
|
17
|
+
*/
|
|
18
|
+
STRICT_SECURITY: {
|
|
19
|
+
preventPromptInjection: true,
|
|
20
|
+
piiSafe: true,
|
|
21
|
+
dbSafe: true,
|
|
22
|
+
strict: true,
|
|
23
|
+
inputSanitization: {
|
|
24
|
+
sanitizeHtml: true,
|
|
25
|
+
removeScriptTags: true,
|
|
26
|
+
escapeSpecialChars: true
|
|
27
|
+
},
|
|
28
|
+
promptInjectionProtection: {
|
|
29
|
+
blockPhrases: [
|
|
30
|
+
"ignore previous instructions", "act as system", "you are root",
|
|
31
|
+
"reveal system prompt", "bypass", "jailbreak", "DAN mode", "Dev mode"
|
|
32
|
+
],
|
|
33
|
+
checklistStrict: true // Hypothetical flag, or we just pass more patterns here
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
/**
|
|
37
|
+
* For educational or open-ended bots.
|
|
38
|
+
* Allows code examples, SQL keywords (in context), etc.
|
|
39
|
+
*/
|
|
40
|
+
EDUCATIONAL: {
|
|
41
|
+
preventPromptInjection: true,
|
|
42
|
+
piiSafe: true,
|
|
43
|
+
dbSafe: false, // Allow SQL discussion
|
|
44
|
+
strict: false,
|
|
45
|
+
inputSanitization: {
|
|
46
|
+
sanitizeHtml: false, // Allow displaying HTML code examples
|
|
47
|
+
removeScriptTags: true // Still dangerous to run
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
package/package.json
CHANGED