trickle-cli 0.1.210 → 0.1.212
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/dist/commands/ci.js +1 -1
- package/dist/commands/compliance.d.ts +1 -1
- package/dist/commands/compliance.js +2 -2
- package/dist/commands/demo.js +1 -1
- package/dist/commands/security.d.ts +1 -1
- package/dist/commands/security.js +122 -2
- package/dist/index.js +1 -1
- package/package.json +18 -2
- package/src/commands/ci.ts +1 -1
- package/src/commands/compliance.ts +2 -2
- package/src/commands/demo.ts +1 -1
- package/src/commands/security.ts +126 -2
- package/src/index.ts +1 -1
package/dist/commands/ci.js
CHANGED
|
@@ -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`;
|
|
@@ -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,
|
package/dist/commands/demo.js
CHANGED
|
@@ -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)) {
|
|
@@ -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
|
-
|
|
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,8 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trickle-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.212",
|
|
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
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"observability",
|
|
7
|
+
"tracing",
|
|
8
|
+
"llm",
|
|
9
|
+
"openai",
|
|
10
|
+
"anthropic",
|
|
11
|
+
"langchain",
|
|
12
|
+
"crewai",
|
|
13
|
+
"agent",
|
|
14
|
+
"mcp",
|
|
15
|
+
"debugging",
|
|
16
|
+
"typescript",
|
|
17
|
+
"python",
|
|
18
|
+
"security",
|
|
19
|
+
"eval",
|
|
20
|
+
"compliance"
|
|
21
|
+
],
|
|
6
22
|
"bin": {
|
|
7
23
|
"trickle": "dist/index.js"
|
|
8
24
|
},
|
package/src/commands/ci.ts
CHANGED
|
@@ -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,
|
package/src/commands/demo.ts
CHANGED
|
@@ -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)) {
|
package/src/commands/security.ts
CHANGED
|
@@ -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
|