trickle-cli 0.1.210 → 0.1.211

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.
@@ -261,7 +261,7 @@ async function postPrComment(alerts, observations, queries, errors, trickleDir)
261
261
  const { runSecurityScan } = require('./security');
262
262
  const origLog2 = console.log;
263
263
  console.log = () => { };
264
- const secResult = runSecurityScan({ dir: trickleDir });
264
+ const secResult = await runSecurityScan({ dir: trickleDir });
265
265
  console.log = origLog2;
266
266
  if (secResult.findings.length > 0) {
267
267
  body += `### 🔒 Security\n\n`;
@@ -14,4 +14,4 @@
14
14
  export declare function generateComplianceReport(opts: {
15
15
  json?: boolean;
16
16
  out?: string;
17
- }): void;
17
+ }): Promise<void>;
@@ -64,7 +64,7 @@ function readJsonl(fp) {
64
64
  return null;
65
65
  } }).filter(Boolean);
66
66
  }
67
- function generateComplianceReport(opts) {
67
+ async function generateComplianceReport(opts) {
68
68
  const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
69
69
  if (!fs.existsSync(dir)) {
70
70
  console.log(chalk_1.default.yellow(' No .trickle/ data. Run trickle first.'));
@@ -119,7 +119,7 @@ function generateComplianceReport(opts) {
119
119
  const { runSecurityScan } = require('./security');
120
120
  const origLog = console.log;
121
121
  console.log = () => { };
122
- const result = runSecurityScan({ dir });
122
+ const result = await runSecurityScan({ dir });
123
123
  console.log = origLog;
124
124
  securityFindings = result.findings.map((f) => ({
125
125
  severity: f.severity, category: f.category,
@@ -165,7 +165,7 @@ async function runDemo() {
165
165
  const { runSecurityScan } = require('./security');
166
166
  const origLog = console.log;
167
167
  console.log = () => { };
168
- const sec = runSecurityScan({ dir: path.join(demoDir, '.trickle') });
168
+ const sec = await runSecurityScan({ dir: path.join(demoDir, '.trickle') });
169
169
  console.log = origLog;
170
170
  if (sec.findings.length > 0) {
171
171
  for (const f of sec.findings.slice(0, 2)) {
@@ -31,5 +31,5 @@ export interface SecurityResult {
31
31
  export declare function runSecurityScan(opts?: {
32
32
  dir?: string;
33
33
  json?: boolean;
34
- }): SecurityResult;
34
+ }): Promise<SecurityResult>;
35
35
  export {};
@@ -86,7 +86,26 @@ const SQLI_PATTERNS = [
86
86
  { name: 'OR 1=1', pattern: /OR\s+['"]?1['"]?\s*=\s*['"]?1/i },
87
87
  { name: 'Comment injection', pattern: /--\s*$|\/\*.*\*\//i },
88
88
  { name: 'DROP/DELETE without WHERE', pattern: /(?:DROP|DELETE\s+FROM)\s+\w+\s*(?:;|$)/i },
89
+ { name: 'Tautology (always true)', pattern: /WHERE\s+(?:1\s*=\s*1|true|''\s*=\s*'')/i },
90
+ { name: 'Stacked queries', pattern: /;\s*(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\s/i },
91
+ { name: 'Sleep/benchmark injection', pattern: /(?:SLEEP|BENCHMARK|WAITFOR\s+DELAY|pg_sleep)\s*\(/i },
89
92
  ];
93
+ // PII patterns — detect personal data in API responses
94
+ const PII_PATTERNS = [
95
+ { name: 'Email address', pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/i, severity: 'info' },
96
+ { name: 'Phone number', pattern: /(?:\+?1[-.\s]?)?\(?[0-9]{3}\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}/, severity: 'info' },
97
+ { name: 'SSN', pattern: /\b\d{3}[-]?\d{2}[-]?\d{4}\b/, severity: 'critical' },
98
+ { name: 'Credit card number', pattern: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b/, severity: 'critical' },
99
+ { name: 'IP address', pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/, severity: 'info' },
100
+ { name: 'Password hash', pattern: /\$2[aby]?\$\d{2}\$[./A-Za-z0-9]{53}/, severity: 'warning' },
101
+ ];
102
+ // Sensitive field names that shouldn't appear in API responses
103
+ const SENSITIVE_FIELDS = new Set([
104
+ 'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
105
+ 'api_secret', 'access_token', 'refresh_token', 'private_key',
106
+ 'credit_card', 'card_number', 'cvv', 'cvc', 'ssn', 'social_security',
107
+ 'pin', 'otp', 'auth_token', 'session_id', 'session_token',
108
+ ]);
90
109
  function scanValue(value, source, location) {
91
110
  const findings = [];
92
111
  const str = typeof value === 'string' ? value : JSON.stringify(value);
@@ -110,7 +129,29 @@ function scanValue(value, source, location) {
110
129
  }
111
130
  return findings;
112
131
  }
113
- function runSecurityScan(opts) {
132
+ /**
133
+ * Recursively extract all field names from a JSON value.
134
+ */
135
+ function extractFieldNames(obj, prefix = '', depth = 0) {
136
+ if (depth > 3 || !obj || typeof obj !== 'object')
137
+ return [];
138
+ const names = [];
139
+ if (Array.isArray(obj)) {
140
+ if (obj.length > 0 && typeof obj[0] === 'object' && obj[0] !== null) {
141
+ names.push(...extractFieldNames(obj[0], prefix, depth + 1));
142
+ }
143
+ }
144
+ else {
145
+ for (const [key, value] of Object.entries(obj)) {
146
+ names.push(key);
147
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
148
+ names.push(...extractFieldNames(value, `${key}.`, depth + 1));
149
+ }
150
+ }
151
+ }
152
+ return names;
153
+ }
154
+ async function runSecurityScan(opts) {
114
155
  const trickleDir = opts?.dir || process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
115
156
  const findings = [];
116
157
  const scanned = { variables: 0, queries: 0, logs: 0, observations: 0 };
@@ -145,8 +186,28 @@ function runSecurityScan(opts) {
145
186
  for (const l of logs) {
146
187
  findings.push(...scanValue(l.message || l.msg, 'log', l.logger || l.name || 'log'));
147
188
  }
148
- // Scan function observations (sample I/O)
189
+ // Scan function observations (sample I/O) — from local file AND backend
149
190
  const observations = readJsonl(path.join(trickleDir, 'observations.jsonl'));
191
+ // Also try to fetch from backend API for richer sample data
192
+ try {
193
+ const { getBackendUrl } = require('../config');
194
+ const backendUrl = getBackendUrl();
195
+ const res = await fetch(`${backendUrl}/api/mock-config`);
196
+ if (res.ok) {
197
+ const { routes } = await res.json();
198
+ for (const r of routes) {
199
+ observations.push({
200
+ functionName: r.functionName,
201
+ module: r.module,
202
+ sampleInput: r.sampleInput,
203
+ sampleOutput: r.sampleOutput,
204
+ });
205
+ }
206
+ }
207
+ }
208
+ catch {
209
+ // Backend not available — use local data only
210
+ }
150
211
  scanned.observations = observations.length;
151
212
  for (const o of observations) {
152
213
  if (o.sampleInput)
@@ -154,6 +215,65 @@ function runSecurityScan(opts) {
154
215
  if (o.sampleOutput)
155
216
  findings.push(...scanValue(o.sampleOutput, 'function_output', `${o.module}.${o.functionName}`));
156
217
  }
218
+ // ── PII in API responses ──
219
+ // Scan sample outputs for personal data that shouldn't be exposed
220
+ for (const o of observations) {
221
+ if (o.sampleOutput) {
222
+ const outputStr = typeof o.sampleOutput === 'string' ? o.sampleOutput : JSON.stringify(o.sampleOutput);
223
+ for (const pii of PII_PATTERNS) {
224
+ if (pii.pattern.test(outputStr)) {
225
+ // Skip email pattern for functions that are SUPPOSED to return emails (e.g., user profile)
226
+ if (pii.name === 'Email address' || pii.name === 'IP address')
227
+ continue; // too noisy for info
228
+ findings.push({
229
+ severity: pii.severity,
230
+ category: 'pii_exposure',
231
+ message: `${pii.name} found in API response: ${o.functionName}`,
232
+ source: 'function_output',
233
+ location: `${o.module}.${o.functionName}`,
234
+ evidence: outputStr.substring(0, 60),
235
+ });
236
+ break;
237
+ }
238
+ }
239
+ }
240
+ }
241
+ // ── Sensitive fields in API responses ──
242
+ // Detect when API responses contain password/token/secret fields
243
+ for (const o of observations) {
244
+ if (o.sampleOutput && typeof o.sampleOutput === 'object') {
245
+ const fields = extractFieldNames(o.sampleOutput);
246
+ for (const field of fields) {
247
+ if (SENSITIVE_FIELDS.has(field.toLowerCase())) {
248
+ findings.push({
249
+ severity: 'critical',
250
+ category: 'sensitive_field_exposure',
251
+ message: `API response "${o.functionName}" contains sensitive field "${field}"`,
252
+ source: 'function_output',
253
+ location: `${o.module}.${o.functionName}`,
254
+ evidence: `field: ${field}`,
255
+ });
256
+ }
257
+ }
258
+ }
259
+ }
260
+ // ── Overprivileged responses (excessive data exposure) ──
261
+ // Flag responses with unusually many fields (potential over-fetching)
262
+ for (const o of observations) {
263
+ if (o.sampleOutput && typeof o.sampleOutput === 'object' && !Array.isArray(o.sampleOutput)) {
264
+ const fields = extractFieldNames(o.sampleOutput);
265
+ if (fields.length > 20) {
266
+ findings.push({
267
+ severity: 'info',
268
+ category: 'excessive_data_exposure',
269
+ message: `API response "${o.functionName}" returns ${fields.length} fields — consider returning only what the client needs`,
270
+ source: 'function_output',
271
+ location: `${o.module}.${o.functionName}`,
272
+ evidence: `${fields.length} fields: ${fields.slice(0, 8).join(', ')}...`,
273
+ });
274
+ }
275
+ }
276
+ }
157
277
  // ── Agent Security: The "Lethal Trifecta" ──
158
278
  // Scan LLM calls for prompt injection and data exfiltration
159
279
  const llmCalls = readJsonl(path.join(trickleDir, 'llm.jsonl'));
package/dist/index.js CHANGED
@@ -683,7 +683,7 @@ program
683
683
  .option("--json", "Structured JSON output")
684
684
  .action(async (opts) => {
685
685
  const { runSecurityScan } = await Promise.resolve().then(() => __importStar(require("./commands/security")));
686
- runSecurityScan({ json: opts.json });
686
+ await runSecurityScan({ json: opts.json });
687
687
  });
688
688
  // trickle deps
689
689
  program
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-cli",
3
- "version": "0.1.210",
3
+ "version": "0.1.211",
4
4
  "description": "Zero-code runtime observability for JS/Python + AI agent debugging. Traces LangChain, CrewAI, OpenAI, Anthropic, Gemini. Eval, security, compliance, cost tracking. Free, local-first.",
5
5
  "keywords": ["observability", "tracing", "llm", "openai", "anthropic", "langchain", "crewai", "agent", "mcp", "debugging", "typescript", "python", "security", "eval", "compliance"],
6
6
  "bin": {
@@ -231,7 +231,7 @@ async function postPrComment(
231
231
  const { runSecurityScan } = require('./security');
232
232
  const origLog2 = console.log;
233
233
  console.log = () => {};
234
- const secResult = runSecurityScan({ dir: trickleDir });
234
+ const secResult = await runSecurityScan({ dir: trickleDir });
235
235
  console.log = origLog2;
236
236
  if (secResult.findings.length > 0) {
237
237
  body += `### 🔒 Security\n\n`;
@@ -67,7 +67,7 @@ interface ComplianceReport {
67
67
  };
68
68
  }
69
69
 
70
- export function generateComplianceReport(opts: { json?: boolean; out?: string }): void {
70
+ export async function generateComplianceReport(opts: { json?: boolean; out?: string }): Promise<void> {
71
71
  const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
72
72
 
73
73
  if (!fs.existsSync(dir)) {
@@ -130,7 +130,7 @@ export function generateComplianceReport(opts: { json?: boolean; out?: string })
130
130
  const { runSecurityScan } = require('./security');
131
131
  const origLog = console.log;
132
132
  console.log = () => {};
133
- const result = runSecurityScan({ dir });
133
+ const result = await runSecurityScan({ dir });
134
134
  console.log = origLog;
135
135
  securityFindings = result.findings.map((f: any) => ({
136
136
  severity: f.severity, category: f.category,
@@ -134,7 +134,7 @@ export async function runDemo(): Promise<void> {
134
134
  const { runSecurityScan } = require('./security');
135
135
  const origLog = console.log;
136
136
  console.log = () => {};
137
- const sec = runSecurityScan({ dir: path.join(demoDir, '.trickle') });
137
+ const sec = await runSecurityScan({ dir: path.join(demoDir, '.trickle') });
138
138
  console.log = origLog;
139
139
  if (sec.findings.length > 0) {
140
140
  for (const f of sec.findings.slice(0, 2)) {
@@ -54,8 +54,29 @@ const SQLI_PATTERNS: Array<{ name: string; pattern: RegExp }> = [
54
54
  { name: 'OR 1=1', pattern: /OR\s+['"]?1['"]?\s*=\s*['"]?1/i },
55
55
  { name: 'Comment injection', pattern: /--\s*$|\/\*.*\*\//i },
56
56
  { name: 'DROP/DELETE without WHERE', pattern: /(?:DROP|DELETE\s+FROM)\s+\w+\s*(?:;|$)/i },
57
+ { name: 'Tautology (always true)', pattern: /WHERE\s+(?:1\s*=\s*1|true|''\s*=\s*'')/i },
58
+ { name: 'Stacked queries', pattern: /;\s*(?:SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER)\s/i },
59
+ { name: 'Sleep/benchmark injection', pattern: /(?:SLEEP|BENCHMARK|WAITFOR\s+DELAY|pg_sleep)\s*\(/i },
57
60
  ];
58
61
 
62
+ // PII patterns — detect personal data in API responses
63
+ const PII_PATTERNS: Array<{ name: string; pattern: RegExp; severity: SecurityFinding['severity'] }> = [
64
+ { name: 'Email address', pattern: /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/i, severity: 'info' },
65
+ { name: 'Phone number', pattern: /(?:\+?1[-.\s]?)?\(?[0-9]{3}\)?[-.\s]?[0-9]{3}[-.\s]?[0-9]{4}/, severity: 'info' },
66
+ { name: 'SSN', pattern: /\b\d{3}[-]?\d{2}[-]?\d{4}\b/, severity: 'critical' },
67
+ { name: 'Credit card number', pattern: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13}|6(?:011|5[0-9]{2})[0-9]{12})\b/, severity: 'critical' },
68
+ { name: 'IP address', pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/, severity: 'info' },
69
+ { name: 'Password hash', pattern: /\$2[aby]?\$\d{2}\$[./A-Za-z0-9]{53}/, severity: 'warning' },
70
+ ];
71
+
72
+ // Sensitive field names that shouldn't appear in API responses
73
+ const SENSITIVE_FIELDS = new Set([
74
+ 'password', 'passwd', 'pwd', 'secret', 'token', 'api_key', 'apikey',
75
+ 'api_secret', 'access_token', 'refresh_token', 'private_key',
76
+ 'credit_card', 'card_number', 'cvv', 'cvc', 'ssn', 'social_security',
77
+ 'pin', 'otp', 'auth_token', 'session_id', 'session_token',
78
+ ]);
79
+
59
80
  function scanValue(value: unknown, source: string, location: string): SecurityFinding[] {
60
81
  const findings: SecurityFinding[] = [];
61
82
  const str = typeof value === 'string' ? value : JSON.stringify(value);
@@ -80,13 +101,34 @@ function scanValue(value: unknown, source: string, location: string): SecurityFi
80
101
  return findings;
81
102
  }
82
103
 
104
+ /**
105
+ * Recursively extract all field names from a JSON value.
106
+ */
107
+ function extractFieldNames(obj: unknown, prefix = '', depth = 0): string[] {
108
+ if (depth > 3 || !obj || typeof obj !== 'object') return [];
109
+ const names: string[] = [];
110
+ if (Array.isArray(obj)) {
111
+ if (obj.length > 0 && typeof obj[0] === 'object' && obj[0] !== null) {
112
+ names.push(...extractFieldNames(obj[0], prefix, depth + 1));
113
+ }
114
+ } else {
115
+ for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
116
+ names.push(key);
117
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
118
+ names.push(...extractFieldNames(value, `${key}.`, depth + 1));
119
+ }
120
+ }
121
+ }
122
+ return names;
123
+ }
124
+
83
125
  export interface SecurityResult {
84
126
  findings: SecurityFinding[];
85
127
  scanned: Record<string, number>;
86
128
  summary: { critical: number; warning: number; info: number };
87
129
  }
88
130
 
89
- export function runSecurityScan(opts?: { dir?: string; json?: boolean }): SecurityResult {
131
+ export async function runSecurityScan(opts?: { dir?: string; json?: boolean }): Promise<SecurityResult> {
90
132
  const trickleDir = opts?.dir || process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
91
133
  const findings: SecurityFinding[] = [];
92
134
  const scanned: Record<string, number> = { variables: 0, queries: 0, logs: 0, observations: 0 };
@@ -125,14 +167,96 @@ export function runSecurityScan(opts?: { dir?: string; json?: boolean }): Securi
125
167
  findings.push(...scanValue(l.message || l.msg, 'log', l.logger || l.name || 'log'));
126
168
  }
127
169
 
128
- // Scan function observations (sample I/O)
170
+ // Scan function observations (sample I/O) — from local file AND backend
129
171
  const observations = readJsonl(path.join(trickleDir, 'observations.jsonl'));
172
+
173
+ // Also try to fetch from backend API for richer sample data
174
+ try {
175
+ const { getBackendUrl } = require('../config');
176
+ const backendUrl = getBackendUrl();
177
+ const res = await fetch(`${backendUrl}/api/mock-config`);
178
+ if (res.ok) {
179
+ const { routes } = await res.json() as { routes: Array<{ functionName: string; module: string; sampleInput: unknown; sampleOutput: unknown }> };
180
+ for (const r of routes) {
181
+ observations.push({
182
+ functionName: r.functionName,
183
+ module: r.module,
184
+ sampleInput: r.sampleInput,
185
+ sampleOutput: r.sampleOutput,
186
+ });
187
+ }
188
+ }
189
+ } catch {
190
+ // Backend not available — use local data only
191
+ }
192
+
130
193
  scanned.observations = observations.length;
131
194
  for (const o of observations) {
132
195
  if (o.sampleInput) findings.push(...scanValue(o.sampleInput, 'function_input', `${o.module}.${o.functionName}`));
133
196
  if (o.sampleOutput) findings.push(...scanValue(o.sampleOutput, 'function_output', `${o.module}.${o.functionName}`));
134
197
  }
135
198
 
199
+ // ── PII in API responses ──
200
+ // Scan sample outputs for personal data that shouldn't be exposed
201
+ for (const o of observations) {
202
+ if (o.sampleOutput) {
203
+ const outputStr = typeof o.sampleOutput === 'string' ? o.sampleOutput : JSON.stringify(o.sampleOutput);
204
+ for (const pii of PII_PATTERNS) {
205
+ if (pii.pattern.test(outputStr)) {
206
+ // Skip email pattern for functions that are SUPPOSED to return emails (e.g., user profile)
207
+ if (pii.name === 'Email address' || pii.name === 'IP address') continue; // too noisy for info
208
+ findings.push({
209
+ severity: pii.severity,
210
+ category: 'pii_exposure',
211
+ message: `${pii.name} found in API response: ${o.functionName}`,
212
+ source: 'function_output',
213
+ location: `${o.module}.${o.functionName}`,
214
+ evidence: outputStr.substring(0, 60),
215
+ });
216
+ break;
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ // ── Sensitive fields in API responses ──
223
+ // Detect when API responses contain password/token/secret fields
224
+ for (const o of observations) {
225
+ if (o.sampleOutput && typeof o.sampleOutput === 'object') {
226
+ const fields = extractFieldNames(o.sampleOutput);
227
+ for (const field of fields) {
228
+ if (SENSITIVE_FIELDS.has(field.toLowerCase())) {
229
+ findings.push({
230
+ severity: 'critical',
231
+ category: 'sensitive_field_exposure',
232
+ message: `API response "${o.functionName}" contains sensitive field "${field}"`,
233
+ source: 'function_output',
234
+ location: `${o.module}.${o.functionName}`,
235
+ evidence: `field: ${field}`,
236
+ });
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ // ── Overprivileged responses (excessive data exposure) ──
243
+ // Flag responses with unusually many fields (potential over-fetching)
244
+ for (const o of observations) {
245
+ if (o.sampleOutput && typeof o.sampleOutput === 'object' && !Array.isArray(o.sampleOutput)) {
246
+ const fields = extractFieldNames(o.sampleOutput);
247
+ if (fields.length > 20) {
248
+ findings.push({
249
+ severity: 'info',
250
+ category: 'excessive_data_exposure',
251
+ message: `API response "${o.functionName}" returns ${fields.length} fields — consider returning only what the client needs`,
252
+ source: 'function_output',
253
+ location: `${o.module}.${o.functionName}`,
254
+ evidence: `${fields.length} fields: ${fields.slice(0, 8).join(', ')}...`,
255
+ });
256
+ }
257
+ }
258
+ }
259
+
136
260
  // ── Agent Security: The "Lethal Trifecta" ──
137
261
 
138
262
  // Scan LLM calls for prompt injection and data exfiltration
package/src/index.ts CHANGED
@@ -700,7 +700,7 @@ program
700
700
  .option("--json", "Structured JSON output")
701
701
  .action(async (opts) => {
702
702
  const { runSecurityScan } = await import("./commands/security");
703
- runSecurityScan({ json: opts.json });
703
+ await runSecurityScan({ json: opts.json });
704
704
  });
705
705
 
706
706
  // trickle deps