cloakllm 0.1.1
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/LICENSE +21 -0
- package/README.md +204 -0
- package/package.json +56 -0
- package/src/audit.js +227 -0
- package/src/cli.js +126 -0
- package/src/config.js +48 -0
- package/src/detector.js +166 -0
- package/src/index.d.ts +160 -0
- package/src/index.js +37 -0
- package/src/llm-detector.js +173 -0
- package/src/middleware.js +237 -0
- package/src/shield.js +126 -0
- package/src/tokenizer.js +133 -0
- package/src/vercel-middleware.js +245 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ziv (Zivuch) Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# CloakLLM
|
|
2
|
+
|
|
3
|
+
**PII cloaking and tamper-evident audit logs for LLM API calls.**
|
|
4
|
+
|
|
5
|
+
CloakLLM intercepts your LLM API calls, detects and cloaks PII before it reaches the provider, and logs every event to a tamper-evident audit chain designed for EU AI Act Article 12 compliance.
|
|
6
|
+
|
|
7
|
+
> **Also available for Python:** `pip install cloakllm` — includes spaCy NER for name/org/location detection. See [CloakLLM Python](https://github.com/cloakllm/CloakLLM-PY).
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install cloakllm
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### With OpenAI SDK (one line)
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
const cloakllm = require('cloakllm');
|
|
21
|
+
const OpenAI = require('openai');
|
|
22
|
+
|
|
23
|
+
const client = new OpenAI();
|
|
24
|
+
cloakllm.enable(client); // That's it. All calls are now cloaked.
|
|
25
|
+
|
|
26
|
+
const response = await client.chat.completions.create({
|
|
27
|
+
model: 'gpt-4o-mini',
|
|
28
|
+
messages: [{
|
|
29
|
+
role: 'user',
|
|
30
|
+
content: 'Write a reminder for sarah.j@techcorp.io about the Q3 audit'
|
|
31
|
+
}]
|
|
32
|
+
});
|
|
33
|
+
// Provider never saw "sarah.j@techcorp.io"
|
|
34
|
+
// Response has the real email restored automatically
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### With Vercel AI SDK
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
const { createCloakLLMMiddleware } = require('cloakllm');
|
|
41
|
+
const { generateText, wrapLanguageModel } = require('ai');
|
|
42
|
+
const { openai } = require('@ai-sdk/openai');
|
|
43
|
+
|
|
44
|
+
const middleware = createCloakLLMMiddleware();
|
|
45
|
+
const model = wrapLanguageModel({ model: openai('gpt-4o'), middleware });
|
|
46
|
+
|
|
47
|
+
const { text } = await generateText({
|
|
48
|
+
model,
|
|
49
|
+
prompt: 'Write a reminder for sarah.j@techcorp.io about the Q3 audit'
|
|
50
|
+
});
|
|
51
|
+
// Provider never saw "sarah.j@techcorp.io"
|
|
52
|
+
// Response has the real email restored automatically
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Works with any AI SDK provider (OpenAI, Anthropic, Google, Mistral, etc.) and supports both `generateText` and `streamText`.
|
|
56
|
+
|
|
57
|
+
### Standalone
|
|
58
|
+
|
|
59
|
+
```javascript
|
|
60
|
+
const { Shield } = require('cloakllm');
|
|
61
|
+
|
|
62
|
+
const shield = new Shield();
|
|
63
|
+
const [sanitized, tokenMap] = shield.sanitize(
|
|
64
|
+
'Send report to john@acme.com, SSN 123-45-6789'
|
|
65
|
+
);
|
|
66
|
+
// sanitized: "Send report to [EMAIL_0], SSN [SSN_0]"
|
|
67
|
+
|
|
68
|
+
// ... send sanitized text to any LLM ...
|
|
69
|
+
|
|
70
|
+
const restored = shield.desanitize(llmResponse, tokenMap);
|
|
71
|
+
// Original values restored
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## What It Detects
|
|
75
|
+
|
|
76
|
+
| Category | Examples | Method |
|
|
77
|
+
|----------|----------|--------|
|
|
78
|
+
| `EMAIL` | `john@acme.com` | Regex |
|
|
79
|
+
| `SSN` | `123-45-6789` | Regex |
|
|
80
|
+
| `CREDIT_CARD` | `4111111111111111` | Regex |
|
|
81
|
+
| `PHONE` | `+1-555-0142` | Regex |
|
|
82
|
+
| `IP_ADDRESS` | `192.168.1.1` | Regex |
|
|
83
|
+
| `API_KEY` | `sk_live_abc123...` | Regex |
|
|
84
|
+
| `AWS_KEY` | `AKIAIOSFODNN7EXAMPLE` | Regex |
|
|
85
|
+
| `JWT` | `eyJhbG...` | Regex |
|
|
86
|
+
| `IBAN` | `DE89370400440532013000` | Regex |
|
|
87
|
+
| `PERSON` | John Smith | LLM (Local) |
|
|
88
|
+
| `ORG` | Acme Corp, Google | LLM (Local) |
|
|
89
|
+
| `GPE` | New York, Israel | LLM (Local) |
|
|
90
|
+
| `ADDRESS` | 742 Evergreen Terrace | LLM (Local) |
|
|
91
|
+
| `DATE_OF_BIRTH` | 1990-01-15 | LLM (Local) |
|
|
92
|
+
| `MEDICAL` | diabetes mellitus | LLM (Local) |
|
|
93
|
+
| `FINANCIAL` | account 4521-XXX | LLM (Local) |
|
|
94
|
+
| `NATIONAL_ID` | TZ 12345678 | LLM (Local) |
|
|
95
|
+
| `BIOMETRIC` | fingerprint hash | LLM (Local) |
|
|
96
|
+
| `USERNAME` | @johndoe42 | LLM (Local) |
|
|
97
|
+
| `PASSWORD` | P@ssw0rd123 | LLM (Local) |
|
|
98
|
+
| `VEHICLE` | plate ABC-1234 | LLM (Local) |
|
|
99
|
+
|
|
100
|
+
> **LLM categories** require opt-in (`llmDetection: true`) and a local [Ollama](https://ollama.com) instance. Data never leaves your machine.
|
|
101
|
+
|
|
102
|
+
## How It Works
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
Your app: "Email sarah.j@techcorp.io about Project Falcon"
|
|
106
|
+
Provider sees: "Email [EMAIL_0] about Project Falcon"
|
|
107
|
+
You receive: Original email restored in the response
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
1. **Detect** — Regex patterns find structured PII (emails, SSNs, credit cards, etc.)
|
|
111
|
+
2. **Cloak** — Replace with deterministic tokens: `[EMAIL_0]`, `[SSN_0]`
|
|
112
|
+
3. **Log** — Write to hash-chained audit trail (each entry includes previous entry's SHA-256 hash)
|
|
113
|
+
4. **Uncloak** — Restore original values in the LLM response
|
|
114
|
+
|
|
115
|
+
## Tamper-Evident Audit Chain
|
|
116
|
+
|
|
117
|
+
Every event is logged to JSONL files with hash chaining:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"seq": 42,
|
|
122
|
+
"event_type": "sanitize",
|
|
123
|
+
"entity_count": 3,
|
|
124
|
+
"categories": {"EMAIL": 1, "SSN": 1, "PHONE": 1},
|
|
125
|
+
"prompt_hash": "sha256:9f86d0...",
|
|
126
|
+
"prev_hash": "sha256:7c4d2e...",
|
|
127
|
+
"entry_hash": "sha256:b5e8f3..."
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Modify any entry and every subsequent hash breaks. Verify with:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npx cloakllm verify ./cloakllm_audit/
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## CLI
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Scan text for PII
|
|
141
|
+
npx cloakllm scan "Email john@test.com, SSN 123-45-6789"
|
|
142
|
+
|
|
143
|
+
# Verify audit chain integrity
|
|
144
|
+
npx cloakllm verify ./cloakllm_audit/
|
|
145
|
+
|
|
146
|
+
# Show audit statistics
|
|
147
|
+
npx cloakllm stats ./cloakllm_audit/
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Configuration
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
const { Shield, ShieldConfig } = require('cloakllm');
|
|
154
|
+
|
|
155
|
+
const shield = new Shield(new ShieldConfig({
|
|
156
|
+
detectEmails: true, // default: true
|
|
157
|
+
detectPhones: true,
|
|
158
|
+
detectSsns: true,
|
|
159
|
+
detectCreditCards: true,
|
|
160
|
+
detectApiKeys: true,
|
|
161
|
+
detectIpAddresses: true,
|
|
162
|
+
detectIban: true,
|
|
163
|
+
logDir: './my-audit-logs', // default: ./cloakllm_audit
|
|
164
|
+
auditEnabled: true, // default: true
|
|
165
|
+
skipModels: ['ollama/'], // skip local models
|
|
166
|
+
customPatterns: [
|
|
167
|
+
{ name: 'EMPLOYEE_ID', pattern: 'EMP-\\d{6}' }
|
|
168
|
+
],
|
|
169
|
+
|
|
170
|
+
// LLM Detection (opt-in, requires Ollama)
|
|
171
|
+
llmDetection: true, // Enable LLM-based detection
|
|
172
|
+
llmModel: 'llama3.2', // Ollama model
|
|
173
|
+
llmOllamaUrl: 'http://localhost:11434', // Ollama endpoint
|
|
174
|
+
llmTimeout: 10000, // Timeout in ms
|
|
175
|
+
llmConfidence: 0.85, // Confidence score
|
|
176
|
+
}));
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## EU AI Act Compliance
|
|
180
|
+
|
|
181
|
+
Article 12 of the EU AI Act requires tamper-evident audit logs for AI systems. Enforcement begins **August 2, 2026**. CloakLLM provides:
|
|
182
|
+
|
|
183
|
+
- **Hash-chained logs** — cryptographically linked, any modification breaks the chain
|
|
184
|
+
- **O(n) verification** — `cloakllm verify` audits the entire chain
|
|
185
|
+
- **No PII in logs** — only hashes and token counts are logged (original values never stored)
|
|
186
|
+
- **Event-level detail** — every sanitize/desanitize event is recorded
|
|
187
|
+
|
|
188
|
+
## Roadmap
|
|
189
|
+
|
|
190
|
+
- [x] NER-based detection (names, orgs, locations) via local LLM
|
|
191
|
+
- [x] Local LLM detection (opt-in, via Ollama)
|
|
192
|
+
- [ ] Streaming response support
|
|
193
|
+
- [x] Vercel AI SDK middleware
|
|
194
|
+
- [ ] LangChain.js integration
|
|
195
|
+
- [ ] OpenTelemetry span emission
|
|
196
|
+
- [ ] RFC 3161 trusted timestamping
|
|
197
|
+
|
|
198
|
+
## License
|
|
199
|
+
|
|
200
|
+
MIT — See [LICENSE](LICENSE).
|
|
201
|
+
|
|
202
|
+
## See Also
|
|
203
|
+
|
|
204
|
+
- **[CloakLLM Python](https://github.com/cloakllm/CloakLLM-PY)** — Python version with spaCy NER + LiteLLM integration
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cloakllm",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "PII cloaking and tamper-evident audit logs for LLM API calls. EU AI Act Article 12 compliance.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"cloakllm": "./src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test test/*.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"llm",
|
|
15
|
+
"privacy",
|
|
16
|
+
"pii",
|
|
17
|
+
"compliance",
|
|
18
|
+
"eu-ai-act",
|
|
19
|
+
"openai",
|
|
20
|
+
"vercel-ai-sdk",
|
|
21
|
+
"audit",
|
|
22
|
+
"security",
|
|
23
|
+
"gdpr",
|
|
24
|
+
"data-protection"
|
|
25
|
+
],
|
|
26
|
+
"author": "Ziv (Zivuch) Chen",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"homepage": "https://github.com/cloakllm/CloakLLM-JS",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/cloakllm/CloakLLM-JS"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/cloakllm/CloakLLM-JS/issues"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src/",
|
|
41
|
+
"LICENSE",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"openai": ">=4.0.0",
|
|
46
|
+
"ai": ">=4.0.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"openai": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"ai": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/audit.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tamper-Evident Audit Logger.
|
|
3
|
+
*
|
|
4
|
+
* Hash-chained append-only JSONL logs for EU AI Act Article 12 compliance.
|
|
5
|
+
* Each entry's SHA-256 hash includes the previous entry's hash.
|
|
6
|
+
* Any modification breaks the chain from that point forward.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const GENESIS_HASH = '0'.repeat(64);
|
|
14
|
+
|
|
15
|
+
class AuditLogger {
|
|
16
|
+
/**
|
|
17
|
+
* @param {import('./config').ShieldConfig} config
|
|
18
|
+
*/
|
|
19
|
+
constructor(config) {
|
|
20
|
+
this.config = config;
|
|
21
|
+
this._seq = 0;
|
|
22
|
+
this._prevHash = GENESIS_HASH;
|
|
23
|
+
this._logDir = config.logDir;
|
|
24
|
+
this._initialized = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_ensureInit() {
|
|
28
|
+
if (this._initialized) return;
|
|
29
|
+
|
|
30
|
+
fs.mkdirSync(this._logDir, { recursive: true });
|
|
31
|
+
|
|
32
|
+
// Recover chain state from most recent log file
|
|
33
|
+
const logFiles = this._getLogFiles();
|
|
34
|
+
if (logFiles.length > 0) {
|
|
35
|
+
const lastFile = logFiles[logFiles.length - 1];
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(lastFile, 'utf-8');
|
|
38
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
39
|
+
if (lines.length > 0) {
|
|
40
|
+
const lastEntry = JSON.parse(lines[lines.length - 1]);
|
|
41
|
+
this._seq = lastEntry.seq + 1;
|
|
42
|
+
this._prevHash = lastEntry.entry_hash;
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Start fresh if corrupted
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this._initialized = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
_getLogFiles() {
|
|
53
|
+
if (!fs.existsSync(this._logDir)) return [];
|
|
54
|
+
return fs.readdirSync(this._logDir)
|
|
55
|
+
.filter(f => f.startsWith('audit_') && f.endsWith('.jsonl'))
|
|
56
|
+
.sort()
|
|
57
|
+
.map(f => path.join(this._logDir, f));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_getLogFile() {
|
|
61
|
+
const today = new Date().toISOString().split('T')[0];
|
|
62
|
+
return path.join(this._logDir, `audit_${today}.jsonl`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compute SHA-256 hash of entry data.
|
|
67
|
+
* @param {Object} data
|
|
68
|
+
* @returns {string}
|
|
69
|
+
*/
|
|
70
|
+
static computeHash(data) {
|
|
71
|
+
// Deterministic serialization: recursively sort keys at all levels
|
|
72
|
+
const sorted = JSON.stringify(data, (_, v) => {
|
|
73
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
74
|
+
return Object.keys(v).sort().reduce((o, k) => { o[k] = v[k]; return o; }, {});
|
|
75
|
+
}
|
|
76
|
+
return v;
|
|
77
|
+
});
|
|
78
|
+
return crypto.createHash('sha256').update(sorted).digest('hex');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Append a new entry to the audit log.
|
|
83
|
+
* @param {Object} options
|
|
84
|
+
* @returns {Object|null}
|
|
85
|
+
*/
|
|
86
|
+
log({
|
|
87
|
+
eventType,
|
|
88
|
+
originalText = '',
|
|
89
|
+
sanitizedText = '',
|
|
90
|
+
model = null,
|
|
91
|
+
provider = null,
|
|
92
|
+
entityCount = 0,
|
|
93
|
+
categories = {},
|
|
94
|
+
tokensUsed = [],
|
|
95
|
+
latencyMs = 0,
|
|
96
|
+
metadata = {},
|
|
97
|
+
}) {
|
|
98
|
+
if (!this.config.auditEnabled) return null;
|
|
99
|
+
|
|
100
|
+
this._ensureInit();
|
|
101
|
+
|
|
102
|
+
const entryData = {
|
|
103
|
+
seq: this._seq,
|
|
104
|
+
event_id: crypto.randomUUID(),
|
|
105
|
+
timestamp: new Date().toISOString(),
|
|
106
|
+
event_type: eventType,
|
|
107
|
+
model,
|
|
108
|
+
provider,
|
|
109
|
+
entity_count: entityCount,
|
|
110
|
+
categories,
|
|
111
|
+
tokens_used: tokensUsed,
|
|
112
|
+
prompt_hash: originalText
|
|
113
|
+
? crypto.createHash('sha256').update(originalText).digest('hex')
|
|
114
|
+
: '',
|
|
115
|
+
sanitized_hash: sanitizedText
|
|
116
|
+
? crypto.createHash('sha256').update(sanitizedText).digest('hex')
|
|
117
|
+
: '',
|
|
118
|
+
latency_ms: Math.round(latencyMs * 100) / 100,
|
|
119
|
+
prev_hash: this._prevHash,
|
|
120
|
+
metadata,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const entryHash = AuditLogger.computeHash(entryData);
|
|
124
|
+
entryData.entry_hash = entryHash;
|
|
125
|
+
|
|
126
|
+
// Write to log file
|
|
127
|
+
const logFile = this._getLogFile();
|
|
128
|
+
fs.appendFileSync(logFile, JSON.stringify(entryData) + '\n');
|
|
129
|
+
|
|
130
|
+
// Update chain state
|
|
131
|
+
this._prevHash = entryHash;
|
|
132
|
+
this._seq += 1;
|
|
133
|
+
|
|
134
|
+
return entryData;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Verify the integrity of the entire audit chain.
|
|
139
|
+
* @param {string} [logFilePath] - Specific file, or all files in logDir
|
|
140
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
141
|
+
*/
|
|
142
|
+
verifyChain(logFilePath = null) {
|
|
143
|
+
const errors = [];
|
|
144
|
+
const files = logFilePath ? [logFilePath] : this._getLogFiles();
|
|
145
|
+
|
|
146
|
+
if (files.length === 0) return { valid: true, errors: [] };
|
|
147
|
+
|
|
148
|
+
let prevHash = GENESIS_HASH;
|
|
149
|
+
|
|
150
|
+
for (const fpath of files) {
|
|
151
|
+
const content = fs.readFileSync(fpath, 'utf-8');
|
|
152
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
153
|
+
const fname = path.basename(fpath);
|
|
154
|
+
|
|
155
|
+
for (let i = 0; i < lines.length; i++) {
|
|
156
|
+
let entry;
|
|
157
|
+
try {
|
|
158
|
+
entry = JSON.parse(lines[i]);
|
|
159
|
+
} catch {
|
|
160
|
+
errors.push(`${fname}:${i + 1} — Invalid JSON`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Check chain link
|
|
165
|
+
if (entry.prev_hash !== prevHash) {
|
|
166
|
+
errors.push(
|
|
167
|
+
`${fname}:${i + 1} seq=${entry.seq} — ` +
|
|
168
|
+
`Chain broken: expected prev_hash=${prevHash.slice(0, 16)}..., ` +
|
|
169
|
+
`got ${(entry.prev_hash || 'MISSING').slice(0, 16)}...`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Recompute entry hash
|
|
174
|
+
const storedHash = entry.entry_hash;
|
|
175
|
+
delete entry.entry_hash;
|
|
176
|
+
const recomputed = AuditLogger.computeHash(entry);
|
|
177
|
+
if (storedHash !== recomputed) {
|
|
178
|
+
errors.push(
|
|
179
|
+
`${fname}:${i + 1} seq=${entry.seq} — ` +
|
|
180
|
+
`Entry tampered: stored_hash=${storedHash.slice(0, 16)}..., ` +
|
|
181
|
+
`recomputed=${recomputed.slice(0, 16)}...`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
prevHash = storedHash;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { valid: errors.length === 0, errors };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get aggregate statistics from audit logs.
|
|
194
|
+
* @returns {Object}
|
|
195
|
+
*/
|
|
196
|
+
getStats() {
|
|
197
|
+
this._ensureInit();
|
|
198
|
+
const stats = {
|
|
199
|
+
total_events: 0,
|
|
200
|
+
total_entities_detected: 0,
|
|
201
|
+
categories: {},
|
|
202
|
+
models_used: new Set(),
|
|
203
|
+
log_files: [],
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
for (const fpath of this._getLogFiles()) {
|
|
207
|
+
stats.log_files.push(path.basename(fpath));
|
|
208
|
+
const content = fs.readFileSync(fpath, 'utf-8');
|
|
209
|
+
for (const line of content.split('\n').filter(l => l.trim())) {
|
|
210
|
+
try {
|
|
211
|
+
const entry = JSON.parse(line);
|
|
212
|
+
stats.total_events += 1;
|
|
213
|
+
stats.total_entities_detected += entry.entity_count || 0;
|
|
214
|
+
for (const [cat, count] of Object.entries(entry.categories || {})) {
|
|
215
|
+
stats.categories[cat] = (stats.categories[cat] || 0) + count;
|
|
216
|
+
}
|
|
217
|
+
if (entry.model) stats.models_used.add(entry.model);
|
|
218
|
+
} catch { /* skip corrupt lines */ }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
stats.models_used = [...stats.models_used];
|
|
223
|
+
return stats;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = { AuditLogger, GENESIS_HASH };
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CloakLLM CLI.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* cloakllm scan "Send email to john@acme.com, SSN 123-45-6789"
|
|
8
|
+
* cloakllm verify ./cloakllm_audit/
|
|
9
|
+
* cloakllm stats ./cloakllm_audit/
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { Shield } = require('./shield');
|
|
13
|
+
const { ShieldConfig } = require('./config');
|
|
14
|
+
const { AuditLogger } = require('./audit');
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const command = args[0];
|
|
18
|
+
|
|
19
|
+
function cmdScan() {
|
|
20
|
+
const text = args.slice(1).join(' ');
|
|
21
|
+
if (!text) {
|
|
22
|
+
console.error('Usage: cloakllm scan "text to scan"');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const config = new ShieldConfig({ auditEnabled: false });
|
|
27
|
+
const shield = new Shield(config);
|
|
28
|
+
|
|
29
|
+
const analysis = shield.analyze(text);
|
|
30
|
+
|
|
31
|
+
if (analysis.entity_count === 0) {
|
|
32
|
+
console.log('✅ No sensitive entities detected.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`⚠️ Found ${analysis.entity_count} sensitive entities:\n`);
|
|
37
|
+
|
|
38
|
+
for (const ent of analysis.entities) {
|
|
39
|
+
console.log(` [${ent.category}] "${ent.text}"`);
|
|
40
|
+
console.log(` Position: ${ent.start}-${ent.end} | Confidence: ${Math.round(ent.confidence * 100)}% | Source: ${ent.source}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const [sanitized, tokenMap] = shield.sanitize(text);
|
|
44
|
+
console.log(`\n${'─'.repeat(60)}`);
|
|
45
|
+
console.log(`ORIGINAL: ${text}`);
|
|
46
|
+
console.log(`SANITIZED: ${sanitized}`);
|
|
47
|
+
console.log(`${'─'.repeat(60)}`);
|
|
48
|
+
console.log(`\nToken map (${tokenMap.entityCount} entities):`);
|
|
49
|
+
for (const [token, original] of tokenMap.reverse) {
|
|
50
|
+
console.log(` ${token} → "${original}"`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function warnIfOutsideCwd(dirPath) {
|
|
55
|
+
const path = require('path');
|
|
56
|
+
const resolved = path.resolve(dirPath);
|
|
57
|
+
const cwd = process.cwd();
|
|
58
|
+
if (!resolved.startsWith(cwd + path.sep) && resolved !== cwd) {
|
|
59
|
+
console.warn(`Warning: Log directory '${resolved}' is outside the current working directory.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function cmdVerify() {
|
|
64
|
+
const logDir = args[1];
|
|
65
|
+
if (!logDir) {
|
|
66
|
+
console.error('Usage: cloakllm verify <log_dir>');
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
warnIfOutsideCwd(logDir);
|
|
71
|
+
|
|
72
|
+
const fs = require('fs');
|
|
73
|
+
if (!fs.existsSync(logDir)) {
|
|
74
|
+
console.error(`❌ Log directory not found: ${logDir}`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const config = new ShieldConfig({ logDir });
|
|
79
|
+
const logger = new AuditLogger(config);
|
|
80
|
+
|
|
81
|
+
console.log(`Verifying audit chain in ${logDir}...`);
|
|
82
|
+
const { valid, errors } = logger.verifyChain();
|
|
83
|
+
|
|
84
|
+
if (valid) {
|
|
85
|
+
console.log('✅ Audit chain integrity verified — no tampering detected.');
|
|
86
|
+
} else {
|
|
87
|
+
console.error(`❌ CHAIN INTEGRITY FAILURE — ${errors.length} error(s):\n`);
|
|
88
|
+
for (const err of errors) {
|
|
89
|
+
console.error(` • ${err}`);
|
|
90
|
+
}
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function cmdStats() {
|
|
96
|
+
const logDir = args[1];
|
|
97
|
+
if (!logDir) {
|
|
98
|
+
console.error('Usage: cloakllm stats <log_dir>');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
warnIfOutsideCwd(logDir);
|
|
103
|
+
|
|
104
|
+
const config = new ShieldConfig({ logDir });
|
|
105
|
+
const logger = new AuditLogger(config);
|
|
106
|
+
console.log(JSON.stringify(logger.getStats(), null, 2));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
switch (command) {
|
|
110
|
+
case 'scan':
|
|
111
|
+
cmdScan();
|
|
112
|
+
break;
|
|
113
|
+
case 'verify':
|
|
114
|
+
cmdVerify();
|
|
115
|
+
break;
|
|
116
|
+
case 'stats':
|
|
117
|
+
cmdStats();
|
|
118
|
+
break;
|
|
119
|
+
default:
|
|
120
|
+
console.log('CloakLLM — AI Compliance Middleware CLI\n');
|
|
121
|
+
console.log('Commands:');
|
|
122
|
+
console.log(' scan <text> Scan text for sensitive data');
|
|
123
|
+
console.log(' verify <dir> Verify audit log integrity');
|
|
124
|
+
console.log(' stats <dir> Show audit statistics');
|
|
125
|
+
break;
|
|
126
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CloakLLM Configuration.
|
|
3
|
+
*
|
|
4
|
+
* All settings have sensible defaults. Override via constructor:
|
|
5
|
+
* const config = new ShieldConfig({ logDir: './my-audit-logs' });
|
|
6
|
+
* const shield = new Shield(config);
|
|
7
|
+
*
|
|
8
|
+
* Or via environment variables:
|
|
9
|
+
* CLOAKLLM_LOG_DIR=./my-audit-logs
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
class ShieldConfig {
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
// --- Detection ---
|
|
15
|
+
this.detectEmails = options.detectEmails ?? true;
|
|
16
|
+
this.detectPhones = options.detectPhones ?? true;
|
|
17
|
+
this.detectSsns = options.detectSsns ?? true;
|
|
18
|
+
this.detectCreditCards = options.detectCreditCards ?? true;
|
|
19
|
+
this.detectApiKeys = options.detectApiKeys ?? true;
|
|
20
|
+
this.detectIpAddresses = options.detectIpAddresses ?? true;
|
|
21
|
+
this.detectIban = options.detectIban ?? true;
|
|
22
|
+
/** @type {Array<{name: string, pattern: string}>} */
|
|
23
|
+
this.customPatterns = options.customPatterns ?? [];
|
|
24
|
+
|
|
25
|
+
// --- LLM Detection (Pass 2: local LLM via Ollama) ---
|
|
26
|
+
this.llmDetection = options.llmDetection ??
|
|
27
|
+
(process.env.CLOAKLLM_LLM_DETECTION ?? 'false').toLowerCase() === 'true';
|
|
28
|
+
this.llmModel = options.llmModel ?? process.env.CLOAKLLM_LLM_MODEL ?? 'llama3.2';
|
|
29
|
+
this.llmOllamaUrl = options.llmOllamaUrl ?? process.env.CLOAKLLM_OLLAMA_URL ?? 'http://localhost:11434';
|
|
30
|
+
this.llmTimeout = options.llmTimeout ?? 10000;
|
|
31
|
+
this.llmConfidence = options.llmConfidence ?? 0.85;
|
|
32
|
+
|
|
33
|
+
// --- Tokenization ---
|
|
34
|
+
this.descriptiveTokens = options.descriptiveTokens ?? true;
|
|
35
|
+
|
|
36
|
+
// --- Audit Logging ---
|
|
37
|
+
this.auditEnabled = options.auditEnabled ?? true;
|
|
38
|
+
this.logDir = options.logDir ?? process.env.CLOAKLLM_LOG_DIR ?? './cloakllm_audit';
|
|
39
|
+
this.logOriginalValues = options.logOriginalValues ?? false;
|
|
40
|
+
|
|
41
|
+
// --- Middleware ---
|
|
42
|
+
this.autoMode = options.autoMode ?? true;
|
|
43
|
+
/** @type {string[]} Model prefixes to skip sanitization */
|
|
44
|
+
this.skipModels = options.skipModels ?? [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { ShieldConfig };
|