@devilsdev/rag-pipeline-utils 2.4.5 → 2.5.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/.ragrc.schema.json +83 -192
- package/CHANGELOG.md +345 -0
- package/README.md +418 -117
- package/SECURITY.md +5 -5
- package/dist/experimental.cjs +2 -0
- package/dist/experimental.d.ts +1 -0
- package/dist/experimental.mjs +5 -0
- package/dist/index.cjs +4992 -1242
- package/dist/index.d.ts +80 -2
- package/dist/index.mjs +1 -1
- package/dist/types/agentic/agentic-pipeline.d.ts +103 -0
- package/dist/types/agentic/iterative-retriever.d.ts +85 -0
- package/dist/types/agentic/query-planner.d.ts +145 -0
- package/dist/types/agentic/self-critique.d.ts +45 -0
- package/dist/types/ai/adaptive-retrieval.d.ts +44 -0
- package/dist/types/ai/federation/federated-learning-coordinator.d.ts +62 -0
- package/dist/types/ai/multimodal-processing.d.ts +32 -0
- package/dist/types/ai/multimodal.d.ts +39 -0
- package/dist/types/ai/retrieval-engine.d.ts +37 -0
- package/dist/types/ai/training/model-training-orchestrator.d.ts +31 -0
- package/dist/types/chunking/chunking-engine.d.ts +70 -0
- package/dist/types/chunking/strategies/fixed-size.d.ts +17 -0
- package/dist/types/chunking/strategies/recursive.d.ts +20 -0
- package/dist/types/chunking/strategies/semantic.d.ts +19 -0
- package/dist/types/chunking/strategies/sentence.d.ts +13 -0
- package/dist/types/chunking/strategies/structure-aware.d.ts +19 -0
- package/dist/types/citation/citation-tracker.d.ts +64 -0
- package/dist/types/citation/hallucination-detector.d.ts +40 -0
- package/dist/types/citation/source-mapper.d.ts +37 -0
- package/dist/types/connectors/anthropic-connector.d.ts +92 -0
- package/dist/types/connectors/base-connector.d.ts +87 -0
- package/dist/types/connectors/cohere-connector.d.ts +89 -0
- package/dist/types/connectors/connector-utils.d.ts +110 -0
- package/dist/types/connectors/local-embedder.d.ts +86 -0
- package/dist/types/connectors/memory-retriever.d.ts +91 -0
- package/dist/types/connectors/ollama-connector.d.ts +110 -0
- package/dist/types/connectors/openai-connector.d.ts +114 -0
- package/dist/types/connectors/rag-prompt.d.ts +40 -0
- package/dist/types/cost/cost-calculator.d.ts +152 -0
- package/dist/types/cost/cost-tracker.d.ts +126 -0
- package/dist/types/cost/token-budget.d.ts +110 -0
- package/dist/types/dag/core/error-context.d.ts +239 -0
- package/dist/types/dag/core/execution-scheduler.d.ts +191 -0
- package/dist/types/dag/core/topology-validation.d.ts +226 -0
- package/dist/types/dag/dag-engine.d.ts +156 -0
- package/dist/types/dag/dag-node.d.ts +37 -0
- package/dist/types/dashboard/dashboard-generator.d.ts +127 -0
- package/dist/types/dashboard/metrics-aggregator.d.ts +218 -0
- package/dist/types/debug/execution-tracer.d.ts +57 -0
- package/dist/types/debug/trace-inspector.d.ts +31 -0
- package/dist/types/ecosystem/plugin-certification.d.ts +169 -0
- package/dist/types/enterprise/data-governance.d.ts +203 -0
- package/dist/types/enterprise/multi-tenancy.d.ts +137 -0
- package/dist/types/enterprise/sso-integration.d.ts +420 -0
- package/dist/types/evaluate/context-metrics.d.ts +55 -0
- package/dist/types/evaluate/faithfulness.d.ts +41 -0
- package/dist/types/evaluate/groundedness.d.ts +60 -0
- package/dist/types/evaluate/pipeline-evaluator.d.ts +88 -0
- package/dist/types/evaluate/relevance.d.ts +21 -0
- package/dist/types/evaluate/scoring.d.ts +123 -0
- package/dist/types/experimental.d.ts +15 -0
- package/dist/types/graph/entity-extractor.d.ts +84 -0
- package/dist/types/graph/graph-index.d.ts +49 -0
- package/dist/types/graph/graph-retriever.d.ts +81 -0
- package/dist/types/graph/knowledge-graph.d.ts +129 -0
- package/dist/types/guardrails/guardrails-pipeline.d.ts +63 -0
- package/dist/types/guardrails/injection-detector.d.ts +47 -0
- package/dist/types/guardrails/post-generation-guard.d.ts +61 -0
- package/dist/types/guardrails/pre-retrieval-guard.d.ts +64 -0
- package/dist/types/guardrails/retrieval-guard.d.ts +57 -0
- package/dist/types/mcp/mcp-server.d.ts +85 -0
- package/dist/types/mcp/mcp-tool-builder.d.ts +55 -0
- package/dist/types/reranker/cascade-reranker.d.ts +40 -0
- package/dist/types/reranker/embedding-reranker.d.ts +77 -0
- package/dist/types/reranker/scoring-reranker.d.ts +41 -0
- package/dist/types/retrieval/bm25-search.d.ts +43 -0
- package/dist/types/retrieval/file-vector-store.d.ts +87 -0
- package/dist/types/retrieval/hybrid-retriever.d.ts +35 -0
- package/dist/types/retrieval/rank-fusion.d.ts +10 -0
- package/dist/types/security/jwt-config.d.ts +80 -0
- package/dist/types/security/jwt-validator.d.ts +195 -0
- package/dist/types/security/plugin-sandbox.d.ts +158 -0
- package/dist/types/security/plugin-signature-verifier.d.ts +139 -0
- package/dist/types/security/validator.d.ts +5 -0
- package/dist/types/streaming/embedding-stream.d.ts +65 -0
- package/dist/types/streaming/sse-adapter.d.ts +59 -0
- package/dist/types/streaming/stream-router.d.ts +88 -0
- package/dist/types/streaming/streaming-embedder.d.ts +99 -0
- package/dist/types/streaming/websocket-adapter.d.ts +71 -0
- package/dist/types/templates/scaffold.d.ts +55 -0
- package/dist/types/templates/template-registry.d.ts +63 -0
- package/dist/types/utils/embed-query.d.ts +20 -0
- package/dist/types/utils/logger.d.ts +50 -0
- package/dist/types/utils/rate-limiter.d.ts +66 -0
- package/dist/types/utils/retry.d.ts +27 -0
- package/dist/types/utils/run-abort.d.ts +34 -0
- package/dist/types/utils/secure-logging.d.ts +51 -0
- package/dist/types/utils/structured-logger.d.ts +126 -0
- package/package.json +53 -29
- package/src/agentic/agentic-pipeline.js +75 -7
- package/src/agentic/iterative-retriever.js +47 -10
- package/src/agentic/query-planner.js +329 -78
- package/src/citation/citation-tracker.js +6 -4
- package/src/citation/hallucination-detector.js +2 -2
- package/src/citation/source-mapper.js +15 -28
- package/src/cli/commands/ai.js +101 -0
- package/src/cli/commands/plugin-hub/event-handlers.js +6 -0
- package/src/cli/commands/plugin-hub/hub-commands.js +0 -74
- package/src/cli/commands/plugin-hub/local-commands.js +143 -0
- package/src/cli/commands/plugin-hub.js +96 -36
- package/src/cli/diagnostics/config-checker.js +76 -63
- package/src/cli/diagnostics/environment-checker.js +62 -31
- package/src/cli/diagnostics/plugin-checker.js +36 -89
- package/src/cli/doctor-command.js +39 -5
- package/src/cli/enhanced-cli-commands.js +72 -217
- package/src/cli/handlers/ai-handler.js +364 -0
- package/src/cli/handlers/diagnostics-handler.js +78 -31
- package/src/cli/handlers/ingest-handler.js +28 -22
- package/src/cli/handlers/init-handler.js +148 -25
- package/src/cli/handlers/query-handler.js +41 -26
- package/src/cli/handlers/run-handler.js +16 -167
- package/src/cli/handlers/serve-handler.js +60 -0
- package/src/cli/services/bench-stats.js +54 -0
- package/src/cli/services/benchmark.js +161 -0
- package/src/cli/services/feedback.js +155 -0
- package/src/cli/services/rag-service.js +322 -0
- package/src/cli/shared/cli-utils.js +24 -7
- package/src/cli/shared/config-context.js +21 -0
- package/src/cli/wizard/config-builder.js +81 -0
- package/src/cli/wizard/init-wizard.js +617 -0
- package/src/config/component-factory.js +125 -0
- package/src/config/load-config.js +29 -10
- package/src/config/load-plugin-config.js +46 -2
- package/src/connectors/anthropic-connector.js +93 -55
- package/src/connectors/base-connector.js +74 -19
- package/src/connectors/cohere-connector.js +88 -32
- package/src/connectors/connector-utils.js +302 -0
- package/src/connectors/local-embedder.js +43 -5
- package/src/connectors/memory-retriever.js +108 -15
- package/src/connectors/ollama-connector.js +83 -49
- package/src/connectors/openai-connector.js +108 -44
- package/src/connectors/rag-prompt.js +88 -0
- package/src/core/create-pipeline.js +209 -60
- package/src/core/plugin-registry.js +87 -11
- package/src/cost/cost-calculator.js +205 -32
- package/src/dev/dev-server.js +166 -11
- package/src/dev/hot-reload.js +40 -1
- package/src/ecosystem/plugin-checks.js +181 -0
- package/src/ecosystem/plugin-hub.js +50 -125
- package/src/ecosystem/plugin-installer.js +298 -0
- package/src/ecosystem/plugin-probe.js +127 -0
- package/src/ecosystem/plugin-scanner.js +185 -0
- package/src/ecosystem/tar-extract.js +114 -0
- package/src/enterprise/audit-logging.js +158 -29
- package/src/enterprise/sso-integration.js +715 -228
- package/src/evaluate/context-metrics.js +28 -16
- package/src/evaluate/evaluator.js +3 -3
- package/src/evaluate/faithfulness.js +18 -14
- package/src/evaluate/groundedness.js +43 -12
- package/src/evaluate/pipeline-evaluator.js +35 -8
- package/src/evaluate/scoring.js +251 -3
- package/src/experimental.js +71 -0
- package/src/guardrails/guardrails-pipeline.js +69 -7
- package/src/guardrails/injection-detector.js +283 -0
- package/src/guardrails/post-generation-guard.js +19 -0
- package/src/guardrails/pre-retrieval-guard.js +36 -36
- package/src/guardrails/retrieval-guard.js +58 -8
- package/src/index.js +36 -1
- package/src/loader/document-reader.js +91 -0
- package/src/reranker/cascade-reranker.js +3 -0
- package/src/reranker/embedding-reranker.js +136 -21
- package/src/reranker/llm-reranker.js +79 -12
- package/src/reranker/scoring-reranker.js +3 -0
- package/src/retrieval/bm25-search.js +12 -6
- package/src/retrieval/file-vector-store.js +212 -0
- package/src/retrieval/hybrid-retriever.js +22 -19
- package/src/security/jwt-config.js +13 -2
- package/src/security/jwt-validator.js +134 -30
- package/src/security/plugin-sandbox.js +18 -1
- package/src/security/plugin-signature-verifier.js +245 -70
- package/src/utils/deprecate.js +69 -0
- package/src/utils/embed-query.js +33 -0
- package/src/utils/input-sanitizer.js +19 -4
- package/src/utils/logger.js +34 -4
- package/src/utils/run-abort.js +102 -0
- package/src/ai/federated-learning.js +0 -211
- package/src/ai/federation.js +0 -71
- package/src/ai/index.js +0 -22
- package/src/ai/model-trainer.js +0 -217
- package/src/ai/model-training.js +0 -202
- package/src/ai/multimodal/multi-modal-processor.js +0 -345
- package/src/ai/orchestrator.js +0 -65
- package/src/ai/retrieval/adaptive-retrieval-engine.js +0 -398
- package/src/cli/commands/ai-ml/adaptive-commands.js +0 -178
- package/src/cli/commands/ai-ml/federated-commands.js +0 -207
- package/src/cli/commands/ai-ml/index.js +0 -17
- package/src/cli/commands/ai-ml/multimodal-commands.js +0 -187
- package/src/cli/commands/ai-ml/shared.js +0 -51
- package/src/cli/commands/ai-ml/training-commands.js +0 -228
- package/src/cli/commands/ai-ml.js +0 -386
- package/src/cli/commands/docs.js +0 -228
- package/src/cli/commands/dx.js +0 -538
- package/src/cli/commands/wizard.js +0 -627
- package/src/cli/input-sanitization-middleware.js +0 -356
- package/src/cli/interactive-wizard.js +0 -373
- package/src/cli/templates/development.template.json +0 -64
- package/src/cli/templates/minimal.template.json +0 -33
- package/src/cli/templates/production.template.json +0 -104
- package/src/cli/templates/testing.template.json +0 -45
- package/src/cli/wizard/configuration.js +0 -148
- package/src/cli/wizard/observability.js +0 -79
- package/src/cli/wizard/performance.js +0 -76
- package/src/cli/wizard/plugin-selection.js +0 -297
- package/src/cli/wizard/preview-save.js +0 -172
- package/src/cli/wizard/project-setup.js +0 -66
- package/src/core/pipeline-factory.js +0 -59
- package/src/dag/enhanced-dag-engine.js +0 -894
- package/src/dx/index.js +0 -40
- package/src/dx/integration-templates.js +0 -662
- package/src/dx/performance-profiler.js +0 -968
- package/src/dx/realtime-debugger.js +0 -755
- package/src/dx/visual-pipeline-builder.js +0 -578
- package/src/ingest.js +0 -59
- package/src/pipeline/pipeline.js +0 -18
- package/src/query.js +0 -117
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Static security scan of a plugin's files. It reads source text only and
|
|
5
|
+
* never executes plugin code. It finds capabilities a plugin uses (running
|
|
6
|
+
* processes, evaluating strings as code, network and filesystem writes) so a
|
|
7
|
+
* user can decide whether to trust it; it is not a sandbox or a malware
|
|
8
|
+
* detector, and obfuscated code can evade it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { relativeWithin } = require('./tar-extract');
|
|
14
|
+
|
|
15
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
16
|
+
const SOURCE_EXTENSIONS = new Set(['.js', '.cjs', '.mjs']);
|
|
17
|
+
|
|
18
|
+
/** severity: high blocks installation unless explicitly allowed */
|
|
19
|
+
const RULES = [
|
|
20
|
+
{
|
|
21
|
+
id: 'eval',
|
|
22
|
+
severity: 'high',
|
|
23
|
+
pattern: /\beval\s*\(/,
|
|
24
|
+
message: 'Evaluates strings as code (eval)',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'new-function',
|
|
28
|
+
severity: 'high',
|
|
29
|
+
pattern: /\bnew\s+Function\s*\(/,
|
|
30
|
+
message: 'Evaluates strings as code (new Function)',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'child-process',
|
|
34
|
+
severity: 'high',
|
|
35
|
+
pattern: /(?:require\s*\(\s*|from\s+)['"](?:node:)?child_process['"]/,
|
|
36
|
+
message: 'Runs external processes (child_process)',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'vm',
|
|
40
|
+
severity: 'high',
|
|
41
|
+
pattern: /(?:require\s*\(\s*|from\s+)['"](?:node:)?vm['"]/,
|
|
42
|
+
message: 'Compiles code at runtime (vm)',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'process-binding',
|
|
46
|
+
severity: 'high',
|
|
47
|
+
pattern: /process\.binding\s*\(/,
|
|
48
|
+
message: 'Uses internal Node bindings (process.binding)',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'dynamic-require',
|
|
52
|
+
severity: 'medium',
|
|
53
|
+
pattern: /\brequire\s*\(\s*[^'"`\s)]/,
|
|
54
|
+
message: 'Loads modules from a computed path',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'network',
|
|
58
|
+
severity: 'medium',
|
|
59
|
+
pattern:
|
|
60
|
+
/(?:require\s*\(\s*|from\s+)['"](?:node:)?(?:http|https|net|tls|dgram)['"]|\bfetch\s*\(|['"]axios['"]/,
|
|
61
|
+
message: 'Makes network requests',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'fs-write',
|
|
65
|
+
severity: 'medium',
|
|
66
|
+
pattern:
|
|
67
|
+
/\b(?:writeFile|appendFile|unlink|rmdir|rm|rename|chmod|createWriteStream)(?:Sync)?\s*\(/,
|
|
68
|
+
message: 'Writes, moves or deletes files',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'env',
|
|
72
|
+
severity: 'low',
|
|
73
|
+
pattern: /process\.env\b/,
|
|
74
|
+
message: 'Reads environment variables (may include API keys)',
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function listFiles(dir) {
|
|
79
|
+
const files = [];
|
|
80
|
+
const walk = (current) => {
|
|
81
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
82
|
+
const full = path.join(current, entry.name);
|
|
83
|
+
if (entry.isSymbolicLink()) continue;
|
|
84
|
+
if (entry.isDirectory()) {
|
|
85
|
+
if (entry.name !== '.git') walk(full);
|
|
86
|
+
} else if (entry.isFile()) {
|
|
87
|
+
files.push(full);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
walk(dir);
|
|
92
|
+
return files;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} pluginPath - Plugin directory or single .js file
|
|
97
|
+
* @returns {{
|
|
98
|
+
* findings: Array<{severity: string, rule: string, message: string, file: string, line?: number}>,
|
|
99
|
+
* counts: {high: number, medium: number, low: number},
|
|
100
|
+
* filesScanned: number
|
|
101
|
+
* }}
|
|
102
|
+
*/
|
|
103
|
+
function scanPlugin(pluginPath) {
|
|
104
|
+
const root = path.resolve(pluginPath);
|
|
105
|
+
const isDir = fs.statSync(root).isDirectory();
|
|
106
|
+
const baseDir = isDir ? root : path.dirname(root);
|
|
107
|
+
const files = isDir ? listFiles(root) : [root];
|
|
108
|
+
const findings = [];
|
|
109
|
+
let filesScanned = 0;
|
|
110
|
+
|
|
111
|
+
const add = (finding) =>
|
|
112
|
+
findings.push({
|
|
113
|
+
...finding,
|
|
114
|
+
file:
|
|
115
|
+
relativeWithin(baseDir, finding.file) || path.basename(finding.file),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
for (const file of files) {
|
|
119
|
+
const ext = path.extname(file).toLowerCase();
|
|
120
|
+
if (ext === '.node') {
|
|
121
|
+
add({
|
|
122
|
+
severity: 'high',
|
|
123
|
+
rule: 'native-addon',
|
|
124
|
+
message: 'Contains a compiled native addon (.node)',
|
|
125
|
+
file,
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (!SOURCE_EXTENSIONS.has(ext)) continue;
|
|
130
|
+
if (fs.statSync(file).size > MAX_FILE_BYTES) {
|
|
131
|
+
add({
|
|
132
|
+
severity: 'medium',
|
|
133
|
+
rule: 'large-file',
|
|
134
|
+
message: 'Source file over 2 MB was not scanned',
|
|
135
|
+
file,
|
|
136
|
+
});
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
filesScanned++;
|
|
140
|
+
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
|
141
|
+
for (const rule of RULES) {
|
|
142
|
+
const index = lines.findIndex((line) => rule.pattern.test(line));
|
|
143
|
+
if (index !== -1) {
|
|
144
|
+
add({
|
|
145
|
+
severity: rule.severity,
|
|
146
|
+
rule: rule.id,
|
|
147
|
+
message: rule.message,
|
|
148
|
+
file,
|
|
149
|
+
line: index + 1,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const manifest = path.join(baseDir, 'package.json');
|
|
156
|
+
if (isDir && fs.existsSync(manifest)) {
|
|
157
|
+
try {
|
|
158
|
+
const scripts =
|
|
159
|
+
JSON.parse(fs.readFileSync(manifest, 'utf8')).scripts || {};
|
|
160
|
+
for (const hook of ['preinstall', 'install', 'postinstall']) {
|
|
161
|
+
if (scripts[hook]) {
|
|
162
|
+
add({
|
|
163
|
+
severity: 'medium',
|
|
164
|
+
rule: 'install-script',
|
|
165
|
+
message: `Declares a "${hook}" script (rag-pipeline never runs it)`,
|
|
166
|
+
file: manifest,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
add({
|
|
172
|
+
severity: 'medium',
|
|
173
|
+
rule: 'bad-manifest',
|
|
174
|
+
message: 'package.json is not valid JSON',
|
|
175
|
+
file: manifest,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const counts = { high: 0, medium: 0, low: 0 };
|
|
181
|
+
for (const finding of findings) counts[finding.severity]++;
|
|
182
|
+
return { findings, counts, filesScanned };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { scanPlugin, RULES };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal .tgz extractor for plugin packages (npm pack format). Extracts
|
|
5
|
+
* regular files and directories only; symlinks, hard links and devices are
|
|
6
|
+
* skipped, and every path must stay inside the destination directory.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const zlib = require('zlib');
|
|
12
|
+
|
|
13
|
+
const BLOCK = 512;
|
|
14
|
+
|
|
15
|
+
/** Separator-agnostic "child is root or inside it" (paths already resolved). */
|
|
16
|
+
function isWithin(root, child) {
|
|
17
|
+
const a = root.replace(/\\/g, '/').replace(/\/$/, '');
|
|
18
|
+
const b = child.replace(/\\/g, '/');
|
|
19
|
+
return b === a || b.startsWith(`${a}/`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readString(buffer, start, length) {
|
|
23
|
+
const slice = buffer.subarray(start, start + length);
|
|
24
|
+
const end = slice.indexOf(0);
|
|
25
|
+
return slice.subarray(0, end === -1 ? slice.length : end).toString('utf8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readOctal(buffer, start, length) {
|
|
29
|
+
const text = readString(buffer, start, length).trim();
|
|
30
|
+
return text ? parseInt(text, 8) : 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Parse "<len> path=<value>\n" records from a pax extended header. */
|
|
34
|
+
function parsePaxPath(data) {
|
|
35
|
+
const match = data.toString('utf8').match(/\d+ path=([^\n]*)\n/);
|
|
36
|
+
return match ? match[1] : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract a gzipped tarball into destDir.
|
|
41
|
+
*
|
|
42
|
+
* @param {Buffer} archive - .tgz contents
|
|
43
|
+
* @param {string} destDir - Target directory (created if missing)
|
|
44
|
+
* @param {object} [options]
|
|
45
|
+
* @param {number} [options.strip=0] - Leading path components to drop (npm packs use "package/")
|
|
46
|
+
* @param {number} [options.maxBytes=50MB] - Refuse archives that expand beyond this
|
|
47
|
+
* @returns {{files: string[], skipped: string[]}}
|
|
48
|
+
*/
|
|
49
|
+
function extractTarGz(archive, destDir, options = {}) {
|
|
50
|
+
const { strip = 0, maxBytes = 50 * 1024 * 1024 } = options;
|
|
51
|
+
const tar = zlib.gunzipSync(archive, { maxOutputLength: maxBytes });
|
|
52
|
+
const root = path.resolve(destDir);
|
|
53
|
+
fs.mkdirSync(root, { recursive: true });
|
|
54
|
+
|
|
55
|
+
const files = [];
|
|
56
|
+
const skipped = [];
|
|
57
|
+
let offset = 0;
|
|
58
|
+
let paxPath = null;
|
|
59
|
+
|
|
60
|
+
while (offset + BLOCK <= tar.length) {
|
|
61
|
+
const header = tar.subarray(offset, offset + BLOCK);
|
|
62
|
+
if (header.every((byte) => byte === 0)) break; // end-of-archive marker
|
|
63
|
+
|
|
64
|
+
const size = readOctal(header, 124, 12);
|
|
65
|
+
const type = String.fromCharCode(header[156] || 48); // '\0' means file
|
|
66
|
+
const prefix = readString(header, 345, 155);
|
|
67
|
+
const headerName = readString(header, 0, 100);
|
|
68
|
+
const name = paxPath || (prefix ? `${prefix}/${headerName}` : headerName);
|
|
69
|
+
const dataStart = offset + BLOCK;
|
|
70
|
+
const data = tar.subarray(dataStart, dataStart + size);
|
|
71
|
+
offset = dataStart + Math.ceil(size / BLOCK) * BLOCK;
|
|
72
|
+
|
|
73
|
+
if (type === 'x') {
|
|
74
|
+
paxPath = parsePaxPath(data);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
paxPath = null;
|
|
78
|
+
if (type === 'g') continue;
|
|
79
|
+
|
|
80
|
+
const parts = name.split('/').filter((p) => p && p !== '.');
|
|
81
|
+
const relative = parts.slice(strip).join('/');
|
|
82
|
+
if (!relative) continue;
|
|
83
|
+
|
|
84
|
+
const target = path.resolve(root, relative);
|
|
85
|
+
if (!isWithin(root, target)) {
|
|
86
|
+
throw new Error(`Archive entry escapes the destination: ${name}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (type === '5') {
|
|
90
|
+
fs.mkdirSync(target, { recursive: true });
|
|
91
|
+
} else if (type === '0' || type === '\0' || type === '7') {
|
|
92
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
93
|
+
fs.writeFileSync(target, data);
|
|
94
|
+
files.push(relative);
|
|
95
|
+
} else {
|
|
96
|
+
skipped.push(relative); // links, devices, fifos
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { files, skipped };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { extractTarGz, isWithin };
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Path of `child` relative to `root` with "/" separators, or null when it is
|
|
106
|
+
* not inside root. Separator-agnostic like isWithin.
|
|
107
|
+
*/
|
|
108
|
+
function relativeWithin(root, child) {
|
|
109
|
+
if (!isWithin(root, child)) return null;
|
|
110
|
+
const a = root.replace(/\\/g, '/').replace(/\/$/, '');
|
|
111
|
+
return child.replace(/\\/g, '/').slice(a.length + 1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports.relativeWithin = relativeWithin;
|
|
@@ -13,6 +13,42 @@ const { EventEmitter } = require('events');
|
|
|
13
13
|
// eslint-disable-line global-require
|
|
14
14
|
const { logger } = require('../utils/logger');
|
|
15
15
|
|
|
16
|
+
const INTEGRITY_VERSION = 2;
|
|
17
|
+
const ENCRYPTION_VERSION = 2;
|
|
18
|
+
const ENCRYPTION_KEY_ENV = 'RAG_AUDIT_ENCRYPTION_KEY';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* OpenSSL EVP_BytesToKey with MD5, one iteration and no salt: the key/IV
|
|
22
|
+
* derivation used by the removed crypto.createCipher/createDecipher.
|
|
23
|
+
* D_1 = MD5(pass), D_i = MD5(D_{i-1} || pass); key || iv = D_1 || D_2 || ...
|
|
24
|
+
*
|
|
25
|
+
* @param {Buffer|string} password - Passphrase given to createCipher
|
|
26
|
+
* @param {number} keyLen - Key length in bytes
|
|
27
|
+
* @param {number} ivLen - IV length in bytes (12 for GCM)
|
|
28
|
+
* @returns {{key: Buffer, iv: Buffer}}
|
|
29
|
+
*/
|
|
30
|
+
function evpBytesToKey(password, keyLen, ivLen) {
|
|
31
|
+
const pass = Buffer.isBuffer(password)
|
|
32
|
+
? password
|
|
33
|
+
: Buffer.from(String(password), 'utf8');
|
|
34
|
+
const blocks = [];
|
|
35
|
+
let total = 0;
|
|
36
|
+
let prev = Buffer.alloc(0);
|
|
37
|
+
while (total < keyLen + ivLen) {
|
|
38
|
+
prev = crypto
|
|
39
|
+
.createHash('md5')
|
|
40
|
+
.update(Buffer.concat([prev, pass]))
|
|
41
|
+
.digest();
|
|
42
|
+
blocks.push(prev);
|
|
43
|
+
total += prev.length;
|
|
44
|
+
}
|
|
45
|
+
const out = Buffer.concat(blocks);
|
|
46
|
+
return {
|
|
47
|
+
key: out.subarray(0, keyLen),
|
|
48
|
+
iv: out.subarray(keyLen, keyLen + ivLen),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
16
52
|
class AuditLogger extends EventEmitter {
|
|
17
53
|
constructor(_options = {}) {
|
|
18
54
|
super();
|
|
@@ -53,8 +89,7 @@ class AuditLogger extends EventEmitter {
|
|
|
53
89
|
};
|
|
54
90
|
|
|
55
91
|
this.logBuffer = [];
|
|
56
|
-
this.encryptionKey =
|
|
57
|
-
_options.encryptionKey || this._generateEncryptionKey();
|
|
92
|
+
this.encryptionKey = this._resolveEncryptionKey(_options);
|
|
58
93
|
this.signingKey = _options.signingKey || this._generateSigningKey();
|
|
59
94
|
this.sequenceNumber = 0;
|
|
60
95
|
|
|
@@ -355,6 +390,7 @@ class AuditLogger extends EventEmitter {
|
|
|
355
390
|
async _writeAuditLog(auditEvent) {
|
|
356
391
|
// Add sequence number for integrity chain
|
|
357
392
|
auditEvent.sequence = ++this.sequenceNumber;
|
|
393
|
+
auditEvent.integrityVersion = INTEGRITY_VERSION;
|
|
358
394
|
|
|
359
395
|
// Calculate integrity hash
|
|
360
396
|
auditEvent.integrity = await this._calculateIntegrityHash(auditEvent);
|
|
@@ -384,9 +420,9 @@ class AuditLogger extends EventEmitter {
|
|
|
384
420
|
|
|
385
421
|
this.emit('audit_logged', {
|
|
386
422
|
_category: auditEvent._category,
|
|
387
|
-
_action: auditEvent.
|
|
423
|
+
_action: auditEvent.action,
|
|
388
424
|
_severity: auditEvent.metadata._severity,
|
|
389
|
-
_correlationId: auditEvent.metadata.
|
|
425
|
+
_correlationId: auditEvent.metadata.correlationId,
|
|
390
426
|
});
|
|
391
427
|
|
|
392
428
|
return auditEvent.metadata.correlationId;
|
|
@@ -490,12 +526,12 @@ class AuditLogger extends EventEmitter {
|
|
|
490
526
|
*/
|
|
491
527
|
async generateComplianceReport(_options) {
|
|
492
528
|
const {
|
|
493
|
-
_tenantId,
|
|
494
529
|
standard, // GDPR, HIPAA, SOC2, PCI-DSS
|
|
495
530
|
startDate,
|
|
496
531
|
endDate,
|
|
497
532
|
includeEvidence = false,
|
|
498
533
|
} = _options;
|
|
534
|
+
const _tenantId = _options.tenantId ?? _options._tenantId;
|
|
499
535
|
|
|
500
536
|
const query = {
|
|
501
537
|
_tenantId,
|
|
@@ -566,7 +602,8 @@ class AuditLogger extends EventEmitter {
|
|
|
566
602
|
* Verify log integrity
|
|
567
603
|
*/
|
|
568
604
|
async verifyIntegrity(_options = {}) {
|
|
569
|
-
const { startDate, endDate
|
|
605
|
+
const { startDate, endDate } = _options;
|
|
606
|
+
const _tenantId = _options.tenantId ?? _options._tenantId;
|
|
570
607
|
|
|
571
608
|
const integrityFile = path.join(
|
|
572
609
|
this._config.logDir,
|
|
@@ -604,7 +641,7 @@ class AuditLogger extends EventEmitter {
|
|
|
604
641
|
const expectedHash = await this._calculateIntegrityHash(log);
|
|
605
642
|
if (log.integrity !== expectedHash) {
|
|
606
643
|
verification.tamperedLogs.push({
|
|
607
|
-
_correlationId: log.metadata.
|
|
644
|
+
_correlationId: log.metadata.correlationId,
|
|
608
645
|
sequence: log.sequence,
|
|
609
646
|
file: file,
|
|
610
647
|
});
|
|
@@ -616,7 +653,7 @@ class AuditLogger extends EventEmitter {
|
|
|
616
653
|
// Verify digital signature if present
|
|
617
654
|
if (log.signature && !(await this._verifySignature(log))) {
|
|
618
655
|
verification.tamperedLogs.push({
|
|
619
|
-
_correlationId: log.metadata.
|
|
656
|
+
_correlationId: log.metadata.correlationId,
|
|
620
657
|
reason: 'invalid_signature',
|
|
621
658
|
file: file,
|
|
622
659
|
});
|
|
@@ -630,11 +667,14 @@ class AuditLogger extends EventEmitter {
|
|
|
630
667
|
|
|
631
668
|
// Private helper methods
|
|
632
669
|
async _calculateIntegrityHash(auditEvent) {
|
|
670
|
+
// Records written before integrityVersion 2 hashed the non-existent
|
|
671
|
+
// _action/_tenantId fields (always undefined); keep verifying them as-is.
|
|
672
|
+
const isV2 = auditEvent.integrityVersion === INTEGRITY_VERSION;
|
|
633
673
|
const data = {
|
|
634
674
|
sequence: auditEvent.sequence,
|
|
635
675
|
category: auditEvent._category,
|
|
636
|
-
action: auditEvent._action,
|
|
637
|
-
tenantId: auditEvent._tenantId,
|
|
676
|
+
action: isV2 ? auditEvent.action : auditEvent._action,
|
|
677
|
+
tenantId: isV2 ? auditEvent.tenantId : auditEvent._tenantId,
|
|
638
678
|
timestamp: auditEvent.metadata.timestamp,
|
|
639
679
|
};
|
|
640
680
|
|
|
@@ -649,7 +689,13 @@ class AuditLogger extends EventEmitter {
|
|
|
649
689
|
|
|
650
690
|
for (const field of sensitiveFields) {
|
|
651
691
|
if (auditEvent[field]) {
|
|
652
|
-
|
|
692
|
+
// Fresh 96-bit IV per field: GCM must never reuse a (key, IV) pair.
|
|
693
|
+
const iv = crypto.randomBytes(12);
|
|
694
|
+
const cipher = crypto.createCipheriv(
|
|
695
|
+
'aes-256-gcm',
|
|
696
|
+
this._getCipherKey(),
|
|
697
|
+
iv,
|
|
698
|
+
);
|
|
653
699
|
let encrypted = cipher.update(
|
|
654
700
|
JSON.stringify(auditEvent[field]),
|
|
655
701
|
'utf8',
|
|
@@ -659,6 +705,8 @@ class AuditLogger extends EventEmitter {
|
|
|
659
705
|
|
|
660
706
|
auditEvent[field] = {
|
|
661
707
|
encrypted: true,
|
|
708
|
+
v: ENCRYPTION_VERSION,
|
|
709
|
+
iv: iv.toString('hex'),
|
|
662
710
|
data: encrypted,
|
|
663
711
|
authTag: cipher.getAuthTag().toString('hex'),
|
|
664
712
|
};
|
|
@@ -674,10 +722,7 @@ class AuditLogger extends EventEmitter {
|
|
|
674
722
|
for (const field of sensitiveFields) {
|
|
675
723
|
if (auditEvent[field] && auditEvent[field].encrypted) {
|
|
676
724
|
try {
|
|
677
|
-
const decipher =
|
|
678
|
-
'aes-256-gcm',
|
|
679
|
-
this.encryptionKey,
|
|
680
|
-
);
|
|
725
|
+
const decipher = this._createDecipher(auditEvent[field]);
|
|
681
726
|
decipher.setAuthTag(Buffer.from(auditEvent[field].authTag, 'hex'));
|
|
682
727
|
|
|
683
728
|
let decrypted = decipher.update(
|
|
@@ -772,8 +817,83 @@ class AuditLogger extends EventEmitter {
|
|
|
772
817
|
.substring(0, 16);
|
|
773
818
|
}
|
|
774
819
|
|
|
775
|
-
|
|
776
|
-
|
|
820
|
+
/**
|
|
821
|
+
* Resolve the field-encryption key.
|
|
822
|
+
*
|
|
823
|
+
* Order: the `encryptionKey` option, then the RAG_AUDIT_ENCRYPTION_KEY
|
|
824
|
+
* environment variable (used as a passphrase, like a string option). A
|
|
825
|
+
* random per-process key is never generated: records written with it
|
|
826
|
+
* could not be decrypted after a restart.
|
|
827
|
+
*
|
|
828
|
+
* If encryption is enabled but no key is configured, encryption is
|
|
829
|
+
* disabled for this instance with a loud warning (an `encryption_disabled`
|
|
830
|
+
* event is also emitted on the next tick). Pass `requireEncryption: true`
|
|
831
|
+
* to throw instead.
|
|
832
|
+
*
|
|
833
|
+
* @param {Object} _options - Constructor options
|
|
834
|
+
* @returns {Buffer|string|null} Key material, or null when unavailable
|
|
835
|
+
* @private
|
|
836
|
+
*/
|
|
837
|
+
_resolveEncryptionKey(_options) {
|
|
838
|
+
const configured =
|
|
839
|
+
_options.encryptionKey || process.env[ENCRYPTION_KEY_ENV] || null;
|
|
840
|
+
if (configured || !this._config.compliance.encryption) {
|
|
841
|
+
return configured;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
const message =
|
|
845
|
+
`AuditLogger: encryption is enabled but no key is configured. Set the ` +
|
|
846
|
+
`encryptionKey option or the ${ENCRYPTION_KEY_ENV} environment variable.`;
|
|
847
|
+
|
|
848
|
+
if (_options.requireEncryption === true) {
|
|
849
|
+
throw new Error(message);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
logger.warn(
|
|
853
|
+
`[AUDIT] WARNING: ${message} Sensitive audit fields (details, data, ` +
|
|
854
|
+
`request) will be written UNENCRYPTED by this instance. Pass ` +
|
|
855
|
+
`requireEncryption: true to make this an error.`,
|
|
856
|
+
);
|
|
857
|
+
this._config.compliance = {
|
|
858
|
+
...this._config.compliance,
|
|
859
|
+
encryption: false,
|
|
860
|
+
};
|
|
861
|
+
process.nextTick(() =>
|
|
862
|
+
this.emit('encryption_disabled', { reason: 'no_encryption_key' }),
|
|
863
|
+
);
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* AES-256 needs exactly 32 key bytes. A 32-byte Buffer is used as-is; any
|
|
869
|
+
* other key (e.g. a passphrase string) is reduced with SHA-256.
|
|
870
|
+
*/
|
|
871
|
+
_getCipherKey() {
|
|
872
|
+
const key = this.encryptionKey;
|
|
873
|
+
if (Buffer.isBuffer(key) && key.length === 32) return key;
|
|
874
|
+
return crypto.createHash('sha256').update(key).digest();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
_createDecipher(encryptedField) {
|
|
878
|
+
if (!this.encryptionKey) {
|
|
879
|
+
throw new Error(
|
|
880
|
+
`No audit encryption key configured (set encryptionKey or ${ENCRYPTION_KEY_ENV})`,
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
if (encryptedField.v === ENCRYPTION_VERSION) {
|
|
884
|
+
return crypto.createDecipheriv(
|
|
885
|
+
'aes-256-gcm',
|
|
886
|
+
this._getCipherKey(),
|
|
887
|
+
Buffer.from(encryptedField.iv, 'hex'),
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
// Legacy (v1) records were written with crypto.createCipher, which was
|
|
891
|
+
// removed in Node 22. Re-derive its key and IV exactly as it did and use
|
|
892
|
+
// createDecipheriv so they decrypt on every supported Node version. The
|
|
893
|
+
// passphrase is the raw key material createCipher was given, not the
|
|
894
|
+
// SHA-256-reduced key used for v2 records.
|
|
895
|
+
const { key, iv } = evpBytesToKey(this.encryptionKey, 32, 12);
|
|
896
|
+
return crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
777
897
|
}
|
|
778
898
|
|
|
779
899
|
_generateSigningKey() {
|
|
@@ -838,8 +958,14 @@ class AuditLogger extends EventEmitter {
|
|
|
838
958
|
|
|
839
959
|
async _verifySignature(auditEvent) {
|
|
840
960
|
const { signature, ...eventData } = auditEvent;
|
|
961
|
+
if (typeof signature !== 'string') return false;
|
|
841
962
|
const expectedSignature = await this._signAuditEvent(eventData);
|
|
842
|
-
|
|
963
|
+
const actual = Buffer.from(signature, 'utf8');
|
|
964
|
+
const expected = Buffer.from(expectedSignature, 'utf8');
|
|
965
|
+
// timingSafeEqual requires equal lengths; a length mismatch is not secret
|
|
966
|
+
// (the expected length is fixed by the HMAC algorithm).
|
|
967
|
+
if (actual.length !== expected.length) return false;
|
|
968
|
+
return crypto.timingSafeEqual(actual, expected);
|
|
843
969
|
}
|
|
844
970
|
|
|
845
971
|
async _getLogFilesInRange(startDate, endDate) {
|
|
@@ -873,17 +999,20 @@ class AuditLogger extends EventEmitter {
|
|
|
873
999
|
}
|
|
874
1000
|
|
|
875
1001
|
_matchesQuery(log, query) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
log.metadata
|
|
885
|
-
|
|
886
|
-
|
|
1002
|
+
// Accept both plain (tenantId) and underscore (_tenantId) query keys; the
|
|
1003
|
+
// underscore form matches the event input names used by the log* methods.
|
|
1004
|
+
const pick = (key) => query[key] ?? query[`_${key}`];
|
|
1005
|
+
const filters = [
|
|
1006
|
+
[pick('tenantId'), log.tenantId],
|
|
1007
|
+
[pick('userId'), log.userId],
|
|
1008
|
+
[pick('category'), log._category],
|
|
1009
|
+
[pick('action'), log.action],
|
|
1010
|
+
[pick('severity'), log.metadata?._severity],
|
|
1011
|
+
[pick('correlationId'), log.metadata?.correlationId],
|
|
1012
|
+
];
|
|
1013
|
+
for (const [wanted, actual] of filters) {
|
|
1014
|
+
if (wanted !== undefined && wanted !== actual) return false;
|
|
1015
|
+
}
|
|
887
1016
|
|
|
888
1017
|
const logTime = new Date(log.metadata.timestamp);
|
|
889
1018
|
if (query.startDate && logTime < new Date(query.startDate)) return false;
|