fix: address code scanning alerts (#240)

This commit is contained in:
Vasanth T
2026-04-03 14:52:35 +05:30
committed by GitHub
parent f3a984dde1
commit 7c0ea68b65
15 changed files with 205 additions and 73 deletions

View File

@@ -6,6 +6,9 @@ on:
branches: branches:
- main - main
permissions:
contents: read
jobs: jobs:
smoke-and-tests: smoke-and-tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest

View File

@@ -199,15 +199,19 @@ export async function submitTranscriptShare() { return { success: false }; }
`, `,
} }
function escapeForResolvedPathRegex(modulePath: string): string {
return modulePath
.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
.replace(/\//g, '[/\\\\]')
}
export const noTelemetryPlugin: BunPlugin = { export const noTelemetryPlugin: BunPlugin = {
name: 'no-telemetry', name: 'no-telemetry',
setup(build) { setup(build) {
for (const [modulePath, contents] of Object.entries(stubs)) { for (const [modulePath, contents] of Object.entries(stubs)) {
// Build regex that matches the resolved file path on any OS // Build regex that matches the resolved file path on any OS
// e.g. "services/analytics/growthbook" → /services[/\\]analytics[/\\]growthbook\.(ts|js)$/ // e.g. "services/analytics/growthbook" → /services[/\\]analytics[/\\]growthbook\.(ts|js)$/
const escaped = modulePath const escaped = escapeForResolvedPathRegex(modulePath)
.replace(/\//g, '[/\\\\]')
.replace(/\./g, '\\.')
const filter = new RegExp(`${escaped}\\.(ts|js)$`) const filter = new RegExp(`${escaped}\\.(ts|js)$`)
build.onLoad({ filter }, () => ({ build.onLoad({ filter }, () => ({

View File

@@ -124,19 +124,15 @@ function printSummary(profile: ProviderProfile, env: NodeJS.ProcessEnv): void {
console.log(`Launching profile: ${profile}`) console.log(`Launching profile: ${profile}`)
if (profile === 'gemini') { if (profile === 'gemini') {
console.log(`GEMINI_MODEL=${env.GEMINI_MODEL}`) console.log(`GEMINI_MODEL=${env.GEMINI_MODEL}`)
console.log(`GEMINI_API_KEY_SET=${Boolean(env.GEMINI_API_KEY)}`)
} else if (profile === 'codex') { } else if (profile === 'codex') {
console.log(`OPENAI_BASE_URL=${env.OPENAI_BASE_URL}`) console.log(`OPENAI_BASE_URL=${env.OPENAI_BASE_URL}`)
console.log(`OPENAI_MODEL=${env.OPENAI_MODEL}`) console.log(`OPENAI_MODEL=${env.OPENAI_MODEL}`)
console.log(`CODEX_API_KEY_SET=${Boolean(resolveCodexApiCredentials(env).apiKey)}`)
} else if (profile === 'atomic-chat') { } else if (profile === 'atomic-chat') {
console.log(`OPENAI_BASE_URL=${env.OPENAI_BASE_URL}`) console.log(`OPENAI_BASE_URL=${env.OPENAI_BASE_URL}`)
console.log(`OPENAI_MODEL=${env.OPENAI_MODEL}`) console.log(`OPENAI_MODEL=${env.OPENAI_MODEL}`)
console.log('OPENAI_API_KEY_SET=false (local provider, no key required)')
} else { } else {
console.log(`OPENAI_BASE_URL=${env.OPENAI_BASE_URL}`) console.log(`OPENAI_BASE_URL=${env.OPENAI_BASE_URL}`)
console.log(`OPENAI_MODEL=${env.OPENAI_MODEL}`) console.log(`OPENAI_MODEL=${env.OPENAI_MODEL}`)
console.log(`OPENAI_API_KEY_SET=${Boolean(env.OPENAI_API_KEY)}`)
} }
} }

View File

@@ -430,6 +430,7 @@ function writeJsonReport(
options: CliOptions, options: CliOptions,
results: CheckResult[], results: CheckResult[],
): void { ): void {
const envSummary = serializeSafeEnvSummary()
const payload = { const payload = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
cwd: process.cwd(), cwd: process.cwd(),
@@ -438,12 +439,24 @@ function writeJsonReport(
passed: results.filter(result => result.ok).length, passed: results.filter(result => result.ok).length,
failed: results.filter(result => !result.ok).length, failed: results.filter(result => !result.ok).length,
}, },
env: serializeSafeEnvSummary(), env: envSummary,
results, results,
} }
if (options.json) { if (options.json) {
console.log(JSON.stringify(payload, null, 2)) console.log(
JSON.stringify(
{
timestamp: payload.timestamp,
cwd: payload.cwd,
summary: payload.summary,
env: '[redacted in console JSON output; use --out-file for the full report]',
results: payload.results,
},
null,
2,
),
)
} }
if (options.outFile) { if (options.outFile) {

View File

@@ -1,4 +1,4 @@
import { randomBytes } from 'crypto' import { randomInt } from 'crypto'
import type { AppState } from './state/AppState.js' import type { AppState } from './state/AppState.js'
import type { AgentId } from './types/ids.js' import type { AgentId } from './types/ids.js'
import { getTaskOutputPath } from './utils/task/diskOutput.js' import { getTaskOutputPath } from './utils/task/diskOutput.js'
@@ -97,10 +97,9 @@ const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
export function generateTaskId(type: TaskType): string { export function generateTaskId(type: TaskType): string {
const prefix = getTaskIdPrefix(type) const prefix = getTaskIdPrefix(type)
const bytes = randomBytes(8)
let id = prefix let id = prefix
for (let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length] id += TASK_ID_ALPHABET[randomInt(TASK_ID_ALPHABET.length)]!
} }
return id return id
} }

View File

@@ -11,7 +11,7 @@ import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopI
import { render } from '../../ink.js'; import { render } from '../../ink.js';
import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js'; import { KeybindingSetup } from '../../keybindings/KeybindingProviderSetup.js';
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js'; import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
import { clearMcpClientConfig, clearServerTokensFromLocalStorage, getMcpClientConfig, readClientSecret, saveMcpClientSecret } from '../../services/mcp/auth.js'; import { clearMcpClientConfig, clearServerTokensFromLocalStorage, readClientSecret, saveMcpClientSecret } from '../../services/mcp/auth.js';
import { doctorAllServers, doctorServer, type McpDoctorReport, type McpDoctorScopeFilter } from '../../services/mcp/doctor.js'; import { doctorAllServers, doctorServer, type McpDoctorReport, type McpDoctorScopeFilter } from '../../services/mcp/doctor.js';
import { connectToServer, getMcpServerConnectionBatchSize } from '../../services/mcp/client.js'; import { connectToServer, getMcpServerConnectionBatchSize } from '../../services/mcp/client.js';
import { addMcpConfig, getAllMcpConfigs, getMcpConfigByName, getMcpConfigsByScope, removeMcpConfig } from '../../services/mcp/config.js'; import { addMcpConfig, getAllMcpConfigs, getMcpConfigByName, getMcpConfigsByScope, removeMcpConfig } from '../../services/mcp/config.js';
@@ -323,9 +323,7 @@ export async function mcpGetHandler(name: string): Promise<void> {
if (server.oauth?.clientId || server.oauth?.callbackPort) { if (server.oauth?.clientId || server.oauth?.callbackPort) {
const parts: string[] = []; const parts: string[] = [];
if (server.oauth.clientId) { if (server.oauth.clientId) {
parts.push('client_id configured'); parts.push('oauth client configured');
const clientConfig = getMcpClientConfig(name, server);
if (clientConfig?.clientSecret) parts.push('client_secret configured');
} }
if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`); if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`);
// biome-ignore lint/suspicious/noConsole:: intentional console output // biome-ignore lint/suspicious/noConsole:: intentional console output
@@ -347,9 +345,7 @@ export async function mcpGetHandler(name: string): Promise<void> {
if (server.oauth?.clientId || server.oauth?.callbackPort) { if (server.oauth?.clientId || server.oauth?.callbackPort) {
const parts: string[] = []; const parts: string[] = [];
if (server.oauth.clientId) { if (server.oauth.clientId) {
parts.push('client_id configured'); parts.push('oauth client configured');
const clientConfig = getMcpClientConfig(name, server);
if (clientConfig?.clientSecret) parts.push('client_secret configured');
} }
if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`); if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`);
// biome-ignore lint/suspicious/noConsole:: intentional console output // biome-ignore lint/suspicious/noConsole:: intentional console output

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,36 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { extractGitHubRepoSlug } from './repoSlug.ts'
test('keeps owner/repo input as-is', () => {
assert.equal(extractGitHubRepoSlug('Gitlawb/openclaude'), 'Gitlawb/openclaude')
})
test('extracts slug from https GitHub URLs', () => {
assert.equal(
extractGitHubRepoSlug('https://github.com/Gitlawb/openclaude'),
'Gitlawb/openclaude',
)
assert.equal(
extractGitHubRepoSlug('https://www.github.com/Gitlawb/openclaude.git'),
'Gitlawb/openclaude',
)
})
test('extracts slug from ssh GitHub URLs', () => {
assert.equal(
extractGitHubRepoSlug('git@github.com:Gitlawb/openclaude.git'),
'Gitlawb/openclaude',
)
assert.equal(
extractGitHubRepoSlug('ssh://git@github.com/Gitlawb/openclaude'),
'Gitlawb/openclaude',
)
})
test('rejects malformed or non-GitHub URLs', () => {
assert.equal(extractGitHubRepoSlug('https://gitlab.com/Gitlawb/openclaude'), null)
assert.equal(extractGitHubRepoSlug('https://github.com/Gitlawb'), null)
assert.equal(extractGitHubRepoSlug('not actually github.com/Gitlawb/openclaude'), null)
})

View File

@@ -0,0 +1,38 @@
export function extractGitHubRepoSlug(value: string): string | null {
const trimmed = value.trim()
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) && !trimmed.includes('github.com')) {
return null
}
if (!trimmed.includes('github.com')) {
return trimmed
}
const sshMatch = trimmed.match(
/^(?:git@|ssh:\/\/git@)(?:www\.)?github\.com[:/](?<owner>[^/:\s]+)\/(?<repo>[^/\s]+?)(?:\.git)?\/?$/i,
)
if (sshMatch?.groups?.owner && sshMatch.groups.repo) {
return `${sshMatch.groups.owner}/${sshMatch.groups.repo}`
}
try {
const parsed = new URL(trimmed)
const hostname = parsed.hostname.toLowerCase()
if (hostname !== 'github.com' && hostname !== 'www.github.com') {
return null
}
const segments = parsed.pathname
.replace(/^\/+|\/+$/g, '')
.split('/')
.filter(Boolean)
if (segments.length < 2) {
return null
}
return `${segments[0]}/${segments[1]}`.replace(/\.git$/i, '')
} catch {
return null
}
}

View File

@@ -10,7 +10,7 @@
*/ */
import type { UUID } from 'crypto' import type { UUID } from 'crypto'
import { randomBytes } from 'crypto' import { randomInt } from 'crypto'
import { import {
OUTPUT_FILE_TAG, OUTPUT_FILE_TAG,
STATUS_TAG, STATUS_TAG,
@@ -73,10 +73,9 @@ const DEFAULT_MAIN_SESSION_AGENT: CustomAgentDefinition = {
const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz' const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
function generateMainSessionTaskId(): string { function generateMainSessionTaskId(): string {
const bytes = randomBytes(8)
let id = 's' let id = 's'
for (let i = 0; i < 8; i++) { for (let i = 0; i < 8; i++) {
id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length] id += TASK_ID_ALPHABET[randomInt(TASK_ID_ALPHABET.length)]!
} }
return id return id
} }

View File

@@ -0,0 +1,40 @@
import { expect, test } from 'bun:test'
import { applySedSubstitution, type SedEditInfo } from './sedEditParser.js'
function sedInfo(pattern: string, replacement: string, extendedRegex = false): SedEditInfo {
return {
filePath: 'example.txt',
pattern,
replacement,
flags: 'g',
extendedRegex,
}
}
test('BRE mode keeps unescaped plus literal', () => {
const result = applySedSubstitution(
'a+b and aaab',
sedInfo('a+b', 'literal-plus'),
)
expect(result).toBe('literal-plus and aaab')
})
test('BRE mode treats escaped plus as one-or-more', () => {
const result = applySedSubstitution(
'abbb and a+b',
sedInfo('ab\\+', 'one-or-more'),
)
expect(result).toBe('one-or-more and a+b')
})
test('BRE mode preserves escaped backslashes', () => {
const result = applySedSubstitution(
String.raw`foo\bar foo/bar`,
sedInfo(String.raw`foo\\bar`, 'backslash-match'),
)
expect(result).toBe('backslash-match foo/bar')
})

View File

@@ -7,18 +7,6 @@ import { randomBytes } from 'crypto'
import { tryParseShellCommand } from '../../utils/bash/shellQuote.js' import { tryParseShellCommand } from '../../utils/bash/shellQuote.js'
// BRE→ERE conversion placeholders (null-byte sentinels, never appear in user input) // BRE→ERE conversion placeholders (null-byte sentinels, never appear in user input)
const BACKSLASH_PLACEHOLDER = '\x00BACKSLASH\x00'
const PLUS_PLACEHOLDER = '\x00PLUS\x00'
const QUESTION_PLACEHOLDER = '\x00QUESTION\x00'
const PIPE_PLACEHOLDER = '\x00PIPE\x00'
const LPAREN_PLACEHOLDER = '\x00LPAREN\x00'
const RPAREN_PLACEHOLDER = '\x00RPAREN\x00'
const BACKSLASH_PLACEHOLDER_RE = new RegExp(BACKSLASH_PLACEHOLDER, 'g')
const PLUS_PLACEHOLDER_RE = new RegExp(PLUS_PLACEHOLDER, 'g')
const QUESTION_PLACEHOLDER_RE = new RegExp(QUESTION_PLACEHOLDER, 'g')
const PIPE_PLACEHOLDER_RE = new RegExp(PIPE_PLACEHOLDER, 'g')
const LPAREN_PLACEHOLDER_RE = new RegExp(LPAREN_PLACEHOLDER, 'g')
const RPAREN_PLACEHOLDER_RE = new RegExp(RPAREN_PLACEHOLDER, 'g')
export type SedEditInfo = { export type SedEditInfo = {
/** The file path being edited */ /** The file path being edited */
@@ -33,6 +21,40 @@ export type SedEditInfo = {
extendedRegex: boolean extendedRegex: boolean
} }
function convertBrePatternToJs(pattern: string): string {
let result = ''
for (let i = 0; i < pattern.length; i++) {
const char = pattern[i]!
if (char === '\\') {
const next = pattern[i + 1]
if (next === undefined) {
result += '\\\\'
continue
}
if (next === '\\') {
result += '\\\\'
} else if ('+?|()'.includes(next)) {
result += next
} else {
result += `\\${next}`
}
i++
continue
}
if ('+?|()'.includes(char)) {
result += `\\${char}`
continue
}
result += char
}
return result
}
/** /**
* Check if a command is a sed in-place edit command * Check if a command is a sed in-place edit command
* Returns true only for simple sed -i 's/pattern/replacement/flags' file commands * Returns true only for simple sed -i 's/pattern/replacement/flags' file commands
@@ -273,28 +295,7 @@ export function applySedSubstitution(
// ERE/JS: + means "one or more", \+ is literal // ERE/JS: + means "one or more", \+ is literal
// We need to convert BRE escaping to ERE for JavaScript regex // We need to convert BRE escaping to ERE for JavaScript regex
if (!sedInfo.extendedRegex) { if (!sedInfo.extendedRegex) {
jsPattern = jsPattern jsPattern = convertBrePatternToJs(jsPattern)
// Step 1: Protect literal backslashes (\\) first - in both BRE and ERE, \\ is literal backslash
.replace(/\\\\/g, BACKSLASH_PLACEHOLDER)
// Step 2: Replace escaped metacharacters with placeholders (these should become unescaped in JS)
.replace(/\\\+/g, PLUS_PLACEHOLDER)
.replace(/\\\?/g, QUESTION_PLACEHOLDER)
.replace(/\\\|/g, PIPE_PLACEHOLDER)
.replace(/\\\(/g, LPAREN_PLACEHOLDER)
.replace(/\\\)/g, RPAREN_PLACEHOLDER)
// Step 3: Escape unescaped metacharacters (these are literal in BRE)
.replace(/\+/g, '\\+')
.replace(/\?/g, '\\?')
.replace(/\|/g, '\\|')
.replace(/\(/g, '\\(')
.replace(/\)/g, '\\)')
// Step 4: Replace placeholders with their JS equivalents
.replace(BACKSLASH_PLACEHOLDER_RE, '\\\\')
.replace(PLUS_PLACEHOLDER_RE, '+')
.replace(QUESTION_PLACEHOLDER_RE, '?')
.replace(PIPE_PLACEHOLDER_RE, '|')
.replace(LPAREN_PLACEHOLDER_RE, '(')
.replace(RPAREN_PLACEHOLDER_RE, ')')
} }
// Unescape sed-specific escapes in replacement // Unescape sed-specific escapes in replacement

View File

@@ -307,10 +307,6 @@ function stripHtmlCommentsFromTokens(tokens: ReturnType<Lexer['lex']>): {
let result = '' let result = ''
let stripped = false let stripped = false
// A well-formed HTML comment span. Non-greedy so multiple comments on the
// same line are matched independently; [\s\S] to span newlines.
const commentSpan = /<!--[\s\S]*?-->/g
for (const token of tokens) { for (const token of tokens) {
if (token.type === 'html') { if (token.type === 'html') {
const trimmed = token.raw.trimStart() const trimmed = token.raw.trimStart()
@@ -318,7 +314,7 @@ function stripHtmlCommentsFromTokens(tokens: ReturnType<Lexer['lex']>): {
// Per CommonMark, a type-2 HTML block ends at the *line* containing // Per CommonMark, a type-2 HTML block ends at the *line* containing
// `-->`, so text after `-->` on that line is part of this token. // `-->`, so text after `-->` on that line is part of this token.
// Strip only the comment spans and keep any residual content. // Strip only the comment spans and keep any residual content.
const residue = token.raw.replace(commentSpan, '') const residue = stripHtmlCommentSpans(token.raw)
stripped = true stripped = true
if (residue.trim().length > 0) { if (residue.trim().length > 0) {
// Residual content exists (e.g. `<!-- note --> Use bun`): keep it. // Residual content exists (e.g. `<!-- note --> Use bun`): keep it.
@@ -333,6 +329,20 @@ function stripHtmlCommentsFromTokens(tokens: ReturnType<Lexer['lex']>): {
return { content: result, stripped } return { content: result, stripped }
} }
function stripHtmlCommentSpans(raw: string): string {
let residue = raw
while (residue.includes('<!--')) {
const updated = residue.replace(/<!--[\s\S]*?-->/g, '')
if (updated === residue) {
break
}
residue = updated
}
return residue
}
/** /**
* Parses raw memory file content into a MemoryFileInfo. Pure function — no I/O. * Parses raw memory file content into a MemoryFileInfo. Pure function — no I/O.
* *
@@ -504,8 +514,7 @@ function extractIncludePathsFromTokens(
const raw = element.raw || '' const raw = element.raw || ''
const trimmed = raw.trimStart() const trimmed = raw.trimStart()
if (trimmed.startsWith('<!--') && trimmed.includes('-->')) { if (trimmed.startsWith('<!--') && trimmed.includes('-->')) {
const commentSpan = /<!--[\s\S]*?-->/g const residue = stripHtmlCommentSpans(raw)
const residue = raw.replace(commentSpan, '')
if (residue.trim().length > 0) { if (residue.trim().length > 0) {
extractPathsFromText(residue) extractPathsFromText(residue)
} }

View File

@@ -159,7 +159,7 @@ export function logError(error: unknown): void {
const err = toError(error) const err = toError(error)
if (feature('HARD_FAIL') && isHardFailMode()) { if (feature('HARD_FAIL') && isHardFailMode()) {
// biome-ignore lint/suspicious/noConsole:: intentional crash output // biome-ignore lint/suspicious/noConsole:: intentional crash output
console.error('[HARD FAIL] logError called with:', err.stack || err.message) console.error('[HARD FAIL] logError called:', err.name || 'Error')
// eslint-disable-next-line custom-rules/no-process-exit // eslint-disable-next-line custom-rules/no-process-exit
process.exit(1) process.exit(1)
} }

View File

@@ -3,7 +3,7 @@
* Inspired by https://github.com/nas5w/random-word-slugs * Inspired by https://github.com/nas5w/random-word-slugs
* with Claude-flavored words * with Claude-flavored words
*/ */
import { randomBytes } from 'crypto' import { randomInt as cryptoRandomInt } from 'crypto'
// Adjectives for slug generation - whimsical and delightful // Adjectives for slug generation - whimsical and delightful
const ADJECTIVES = [ const ADJECTIVES = [
@@ -765,10 +765,7 @@ const VERBS = [
* Generate a cryptographically random integer in the range [0, max) * Generate a cryptographically random integer in the range [0, max)
*/ */
function randomInt(max: number): number { function randomInt(max: number): number {
// Use crypto.randomBytes for better randomness than Math.random return cryptoRandomInt(max)
const bytes = randomBytes(4)
const value = bytes.readUInt32BE(0)
return value % max
} }
/** /**