Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions scripts/security-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ const IGNORED_VULNERABLE_PACKAGES = [
"jsondiffpatch", // XSS in ai dependency - tracked separately
"undici", // DoS in transitive dependency - requires upstream fix
"ai", // File upload bypass - planned upgrade
"lodash", // Code injection in @semantic-release dev dep - no patch available
"lodash-es", // Code injection in @semantic-release dev dep - no patch available
"lodash", // TODO: track in #xxx — no patch available upstream for @semantic-release dev dep
"lodash-es", // TODO: track in #xxx — no patch available upstream for @semantic-release dev dep
Comment on lines +45 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the pass condition is currently "any ignored package present"
rg -n -C4 'isIgnoredPackage|some\(|results\.dependencies\.status = "passed"|return;' scripts/security-check.ts

Repository: juspay/neurolink

Length of output: 2628


🏁 Script executed:

# Verify lines 45-46 show the IGNORED_VULNERABLE_PACKAGES constant
sed -n '40,55p' scripts/security-check.ts

Repository: juspay/neurolink

Length of output: 691


🏁 Script executed:

# Check full vulnerability detection and filtering logic to ensure no other guards exist
sed -n '145,210p' scripts/security-check.ts

Repository: juspay/neurolink

Length of output: 2525


Ignored-package expansion can incorrectly pass audits with real vulnerabilities

The current logic at line 148–166 uses .some() to check if ANY ignored package exists in the audit output. If true, the scan immediately returns with status = "passed" at line 165, skipping the severity check entirely. This means adding lodash and lodash-es (common packages) will cause any audit containing them to pass, even if non-ignored high/critical vulnerabilities are also present.

Suggested fix (only pass when all detected vulnerable packages are ignored)
-        const isIgnoredPackage = IGNORED_VULNERABLE_PACKAGES.some(
-          (pkg) =>
-            output.includes(`│ Package             │ ${pkg}`) ||
-            output.includes(`Package: ${pkg}`),
-        );
-
-        if (isIgnoredPackage) {
+        const detectedPackages = Array.from(
+          new Set(
+            [...output.matchAll(/Package:\s+([^\s]+)/g)].map((m) => m[1]),
+          ),
+        );
+        const allDetectedAreIgnored =
+          detectedPackages.length > 0 &&
+          detectedPackages.every((pkg) =>
+            IGNORED_VULNERABLE_PACKAGES.includes(pkg),
+          );
+
+        if (allDetectedAreIgnored) {
           const ignoredList = IGNORED_VULNERABLE_PACKAGES.join(", ");
           this.log(
             `Found vulnerabilities in temporarily ignored packages: ${ignoredList}`,
             "cyan",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/security-check.ts` around lines 45 - 46, The check that uses .some()
to decide a scan "passed" when any ignored package appears is incorrect; update
the logic in scripts/security-check.ts (the block that inspects audit results,
uses ignoredPackages and sets status = "passed") so it only returns passed when
every detected vulnerable package is contained in ignoredPackages — e.g.,
collect the list of vulnerable package names from the audit output and replace
the .some() test with a subset check (or .every()) that ensures all detected
vuln packages are ignored before setting status = "passed".

];

interface SecurityIssue {
Expand Down
31 changes: 27 additions & 4 deletions src/cli/commands/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3034,6 +3034,19 @@ ${tools.length > 0 ? tools.map((t) => `- **${t}**: TODO: Add description`).join(

const serverId = argv.server as string | undefined;
const foundTool = this.findToolForAnnotation(servers, toolName, serverId);
if (foundTool === "ambiguous") {
logger.error(
chalk.red(
`Tool '${toolName}' exists on multiple servers. Use --server <id> to specify which server to annotate.`,
),
);
logger.always(
chalk.yellow(
"Use 'neurolink mcp annotate --list' to see available tools and their server IDs.",
),
);
process.exit(1);
}
if (!foundTool) {
logger.error(
chalk.red(
Expand Down Expand Up @@ -3171,25 +3184,35 @@ ${tools.length > 0 ? tools.map((t) => `- **${t}**: TODO: Add description`).join(
servers: MCPServerInfo[],
toolName: string,
serverId?: string,
): AnnotatedToolTarget | null {
): AnnotatedToolTarget | null | "ambiguous" {
const matches: AnnotatedToolTarget[] = [];

for (const server of servers) {
if (serverId && server.id !== serverId) {
continue;
}

for (const tool of server.tools || []) {
if (tool.name === toolName) {
return {
matches.push({
name: tool.name,
description: tool.description,
serverId: server.id,
serverName: server.name,
};
});
}
}
}

return null;
if (matches.length === 0) {
return null;
}

if (matches.length > 1) {
return "ambiguous";
}

return matches[0] ?? null;
}

private static buildAnnotationsFromArgs(
Expand Down
8 changes: 4 additions & 4 deletions src/lib/core/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ProviderRegistry } from "../factories/providerRegistry.js";
import { getBestProvider } from "../utils/providerUtils.js";
import { logger } from "../utils/logger.js";
import { dynamicModelProvider } from "./dynamicModels.js";
import { withTimeout } from "../utils/errorHandling.js";
import { withTimeout, ErrorFactory } from "../utils/errorHandling.js";
import type { AIProvider, SupportedModelName } from "../types/index.js";
import { AIProviderName } from "../constants/enums.js";
import type { UnknownRecord } from "../types/common.js";
Expand Down Expand Up @@ -47,7 +47,7 @@ export class AIProviderFactory {
await withTimeout(
dynamicModelProvider.initialize(),
INIT_TIMEOUT,
new Error("Dynamic provider initialization timeout"),
ErrorFactory.toolTimeout("dynamic-provider-init", INIT_TIMEOUT),
);

logger.debug(
Expand Down Expand Up @@ -251,7 +251,7 @@ export class AIProviderFactory {
await withTimeout(
ProviderRegistry.registerAllProviders(),
30_000,
new Error("Provider registration timed out"),
ErrorFactory.toolTimeout("provider-registration", 30_000),
);

const normalizedName = this.normalizeProviderName(providerName);
Expand All @@ -275,7 +275,7 @@ export class AIProviderFactory {
region,
),
30_000,
new Error(`Provider creation timed out for ${normalizedName}`),
ErrorFactory.toolTimeout(`provider-creation:${normalizedName}`, 30_000),
);

return { normalizedName, finalModelName, provider };
Expand Down
5 changes: 3 additions & 2 deletions src/lib/evaluation/scorers/scorerRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,8 +473,9 @@ export class ScorerRegistry {
logger.debug(
`Registered ${ScorerRegistry.scorers.size} built-in scorers (including aliases)`,
);
} finally {
// Keep initPromise for future callers to await
} catch (err) {
ScorerRegistry.initPromise = null; // allow retry on next call
throw err;
}
})();

Expand Down
14 changes: 11 additions & 3 deletions src/lib/proxy/requestLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,16 +641,24 @@ export async function logBodyCapture(
const redactedHeaders = redactHeaders(entry.headers);
const preparedBody = prepareRedactedBody(entry.body);

let stored: StoredBodyArtifact = {};
let stored: StoredBodyArtifact;
try {
stored = await writeBodyArtifact(
entry,
redactedHeaders,
preparedBody.value,
preparedBody.truncated,
);
} catch {
// Best-effort artifact persistence; continue with in-memory metadata only.
} catch (writeError) {
logger.warn(
"[RequestLogger] writeBodyArtifact failed, falling back to in-memory body for OTLP",
{ error: writeError },
);
stored = {
redactedBody: preparedBody.value,
redactedBodyBytes: preparedBody.bytes,
bodyTruncated: preparedBody.truncated,
};
}

const dateStr = new Date(entry.timestamp).toISOString().split("T")[0];
Expand Down
58 changes: 36 additions & 22 deletions src/lib/tasks/store/redisTaskStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,27 +245,41 @@ export class RedisTaskStore implements TaskStore {
const ttlSeconds = Math.ceil(ttlMs / 1000);
// Set TTL on associated keys best-effort. A successful task write should not
// be surfaced as a failure just because the retention metadata could not be updated.
client.expire(taskRunsKey(task.id), ttlSeconds).catch((err) => {
logger.warn(
"[TaskStore:Redis] Failed to set TTL on task runs key — task data may outlive retention window",
{
taskId: task.id,
key: taskRunsKey(task.id),
ttlSeconds,
error: String(err),
},
);
});
client.expire(taskHistoryKey(task.id), ttlSeconds).catch((err) => {
logger.warn(
"[TaskStore:Redis] Failed to set TTL on task history key — task data may outlive retention window",
{
taskId: task.id,
key: taskHistoryKey(task.id),
ttlSeconds,
error: String(err),
},
);
});
void (async () => {
const runsKey = taskRunsKey(task.id);
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await client.expire(runsKey, ttlSeconds);
break;
} catch (err) {
if (attempt === 3) {
logger.warn(
"[TaskStore:Redis] expire failed after 3 attempts on task runs key — task data may outlive retention window",
{ taskId: task.id, key: runsKey, ttlSeconds, err: String(err) },
);
} else {
await new Promise((r) => setTimeout(r, 100 * attempt));
}
}
}
})();
void (async () => {
const histKey = taskHistoryKey(task.id);
for (let attempt = 1; attempt <= 3; attempt++) {
try {
await client.expire(histKey, ttlSeconds);
break;
} catch (err) {
if (attempt === 3) {
logger.warn(
"[TaskStore:Redis] expire failed after 3 attempts on task history key — task data may outlive retention window",
{ taskId: task.id, key: histKey, ttlSeconds, err: String(err) },
);
} else {
await new Promise((r) => setTimeout(r, 100 * attempt));
}
}
}
})();
}
}
Loading