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:
- main
permissions:
contents: read
jobs:
smoke-and-tests:
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 = {
name: 'no-telemetry',
setup(build) {
for (const [modulePath, contents] of Object.entries(stubs)) {
// Build regex that matches the resolved file path on any OS
// e.g. "services/analytics/growthbook" → /services[/\\]analytics[/\\]growthbook\.(ts|js)$/
const escaped = modulePath
.replace(/\//g, '[/\\\\]')
.replace(/\./g, '\\.')
const escaped = escapeForResolvedPathRegex(modulePath)
const filter = new RegExp(`${escaped}\\.(ts|js)$`)
build.onLoad({ filter }, () => ({

View File

@@ -124,19 +124,15 @@ function printSummary(profile: ProviderProfile, env: NodeJS.ProcessEnv): void {
console.log(`Launching profile: ${profile}`)
if (profile === 'gemini') {
console.log(`GEMINI_MODEL=${env.GEMINI_MODEL}`)
console.log(`GEMINI_API_KEY_SET=${Boolean(env.GEMINI_API_KEY)}`)
} else if (profile === 'codex') {
console.log(`OPENAI_BASE_URL=${env.OPENAI_BASE_URL}`)
console.log(`OPENAI_MODEL=${env.OPENAI_MODEL}`)
console.log(`CODEX_API_KEY_SET=${Boolean(resolveCodexApiCredentials(env).apiKey)}`)
} else if (profile === 'atomic-chat') {
console.log(`OPENAI_BASE_URL=${env.OPENAI_BASE_URL}`)
console.log(`OPENAI_MODEL=${env.OPENAI_MODEL}`)
console.log('OPENAI_API_KEY_SET=false (local provider, no key required)')
} else {
console.log(`OPENAI_BASE_URL=${env.OPENAI_BASE_URL}`)
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,
results: CheckResult[],
): void {
const envSummary = serializeSafeEnvSummary()
const payload = {
timestamp: new Date().toISOString(),
cwd: process.cwd(),
@@ -438,12 +439,24 @@ function writeJsonReport(
passed: results.filter(result => result.ok).length,
failed: results.filter(result => !result.ok).length,
},
env: serializeSafeEnvSummary(),
env: envSummary,
results,
}
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) {

View File

@@ -1,4 +1,4 @@
import { randomBytes } from 'crypto'
import { randomInt } from 'crypto'
import type { AppState } from './state/AppState.js'
import type { AgentId } from './types/ids.js'
import { getTaskOutputPath } from './utils/task/diskOutput.js'
@@ -97,10 +97,9 @@ const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
export function generateTaskId(type: TaskType): string {
const prefix = getTaskIdPrefix(type)
const bytes = randomBytes(8)
let id = prefix
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
}

View File

@@ -11,7 +11,7 @@ import { MCPServerDesktopImportDialog } from '../../components/MCPServerDesktopI
import { render } from '../../ink.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 { 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 { connectToServer, getMcpServerConnectionBatchSize } from '../../services/mcp/client.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) {
const parts: string[] = [];
if (server.oauth.clientId) {
parts.push('client_id configured');
const clientConfig = getMcpClientConfig(name, server);
if (clientConfig?.clientSecret) parts.push('client_secret configured');
parts.push('oauth client configured');
}
if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`);
// 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) {
const parts: string[] = [];
if (server.oauth.clientId) {
parts.push('client_id configured');
const clientConfig = getMcpClientConfig(name, server);
if (clientConfig?.clientSecret) parts.push('client_secret configured');
parts.push('oauth client configured');
}
if (server.oauth.callbackPort) parts.push(`callback_port ${server.oauth.callbackPort}`);
// 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 { randomBytes } from 'crypto'
import { randomInt } from 'crypto'
import {
OUTPUT_FILE_TAG,
STATUS_TAG,
@@ -73,10 +73,9 @@ const DEFAULT_MAIN_SESSION_AGENT: CustomAgentDefinition = {
const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'
function generateMainSessionTaskId(): string {
const bytes = randomBytes(8)
let id = 's'
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
}

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'
// 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 = {
/** The file path being edited */
@@ -33,6 +21,40 @@ export type SedEditInfo = {
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
* 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
// We need to convert BRE escaping to ERE for JavaScript regex
if (!sedInfo.extendedRegex) {
jsPattern = 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, ')')
jsPattern = convertBrePatternToJs(jsPattern)
}
// Unescape sed-specific escapes in replacement

View File

@@ -307,10 +307,6 @@ function stripHtmlCommentsFromTokens(tokens: ReturnType<Lexer['lex']>): {
let result = ''
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) {
if (token.type === 'html') {
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
// `-->`, so text after `-->` on that line is part of this token.
// Strip only the comment spans and keep any residual content.
const residue = token.raw.replace(commentSpan, '')
const residue = stripHtmlCommentSpans(token.raw)
stripped = true
if (residue.trim().length > 0) {
// Residual content exists (e.g. `<!-- note --> Use bun`): keep it.
@@ -333,6 +329,20 @@ function stripHtmlCommentsFromTokens(tokens: ReturnType<Lexer['lex']>): {
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.
*
@@ -504,8 +514,7 @@ function extractIncludePathsFromTokens(
const raw = element.raw || ''
const trimmed = raw.trimStart()
if (trimmed.startsWith('<!--') && trimmed.includes('-->')) {
const commentSpan = /<!--[\s\S]*?-->/g
const residue = raw.replace(commentSpan, '')
const residue = stripHtmlCommentSpans(raw)
if (residue.trim().length > 0) {
extractPathsFromText(residue)
}

View File

@@ -159,7 +159,7 @@ export function logError(error: unknown): void {
const err = toError(error)
if (feature('HARD_FAIL') && isHardFailMode()) {
// 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
process.exit(1)
}

View File

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