diff --git a/CHANGELOG.md b/CHANGELOG.md index ef225ac6f..282d4fbfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [1.1.22](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.22) - 2025-09-20 + +### Changed +- Rename `--only-compute` flag to `--dont-apply-fixes` for `socket fix`, but keep old flag as an alias. + +### Fixed +- Sanitize extracted git repository names to be compatible with the Socket API. ## [1.1.21](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.22) - 2025-09-20 diff --git a/package.json b/package.json index 464289ccb..ad6b40c4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.21", + "version": "1.1.22", "description": "CLI for Socket.dev", "homepage": "https://github.com/SocketDev/socket-cli", "license": "MIT AND OFL-1.1", diff --git a/src/commands/fix/cmd-fix.mts b/src/commands/fix/cmd-fix.mts index 4f0ad8fd3..00637ab36 100644 --- a/src/commands/fix/cmd-fix.mts +++ b/src/commands/fix/cmd-fix.mts @@ -52,6 +52,13 @@ const generalFlags: MeowFlags = { 'https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-auto-merge-for-pull-requests-in-your-repository', )} for managing auto-merge for pull requests in your repository.`, }, + dontApplyFixes: { + aliases: ['onlyCompute'], + type: 'boolean', + default: false, + description: + 'Compute fixes only, do not apply them. Logs what upgrades would be applied. If combined with --output-file, the output file will contain the upgrades that would be applied.', + }, id: { type: 'string', default: [], @@ -86,12 +93,6 @@ Available styles: * preserve - Retain the existing version range style as-is `.trim(), }, - onlyCompute: { - type: 'boolean', - default: false, - description: - 'Compute fixes only, do not apply them. Logs what upgrades would be applied. If combined with --output-file, the output file will contain the upgrades that would be applied.', - }, outputFile: { type: 'string', default: '', @@ -208,12 +209,12 @@ async function run( const { autopilot, + dontApplyFixes, glob, json, limit, markdown, maxSatisfying, - onlyCompute, outputFile, prCheck, rangeStyle, @@ -222,6 +223,7 @@ async function run( unknownFlags = [], } = cli.flags as { autopilot: boolean + dontApplyFixes: boolean glob: string limit: number json: boolean @@ -232,7 +234,6 @@ async function run( rangeStyle: RangeStyle unknownFlags?: string[] outputFile: string - onlyCompute: boolean } const dryRun = !!cli.flags['dryRun'] @@ -291,6 +292,7 @@ async function run( await handleFix({ autopilot, + dontApplyFixes, cwd, ghsas, glob, @@ -302,7 +304,6 @@ async function run( rangeStyle, spinner, unknownFlags, - onlyCompute, outputFile, }) } diff --git a/src/commands/fix/cmd-fix.test.mts b/src/commands/fix/cmd-fix.test.mts index d32da1e6c..fe3a5784e 100644 --- a/src/commands/fix/cmd-fix.test.mts +++ b/src/commands/fix/cmd-fix.test.mts @@ -172,6 +172,7 @@ describe('socket fix', async () => { Options --autopilot Enable auto-merge for pull requests that Socket opens. See GitHub documentation (https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-auto-merge-for-pull-requests-in-your-repository) for managing auto-merge for pull requests in your repository. + --dont-apply-fixes Compute fixes only, do not apply them. Logs what upgrades would be applied. If combined with --output-file, the output file will contain the upgrades that would be applied. --id Provide a list of vulnerability identifiers to compute fixes for: - GHSA IDs (https://docs.github.com/en/code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database/about-the-github-advisory-database#about-ghsa-ids) (e.g., GHSA-xxxx-xxxx-xxxx) - CVE IDs (https://cve.mitre.org/cve/identifiers/) (e.g., CVE-2025-1234) - automatically converted to GHSA @@ -180,7 +181,6 @@ describe('socket fix', async () => { --json Output result as json --limit The number of fixes to attempt at a time (default 10) --markdown Output result as markdown - --only-compute Compute fixes only, do not apply them. Logs what upgrades would be applied. If combined with --output-file, the output file will contain the upgrades that would be applied. --output-file Path to store upgrades as a JSON file at this path. --range-style Define how dependency version ranges are updated in package.json (default 'preserve'). Available styles: diff --git a/src/commands/fix/coana-fix.mts b/src/commands/fix/coana-fix.mts index bc433c2f5..5365d3c97 100644 --- a/src/commands/fix/coana-fix.mts +++ b/src/commands/fix/coana-fix.mts @@ -45,10 +45,10 @@ export async function coanaFix( const { autopilot, cwd, + dontApplyFixes, ghsas, glob, limit, - onlyCompute, orgSlug, outputFile, spinner, @@ -106,7 +106,7 @@ export async function coanaFix( if (!shouldOpenPrs) { // Inform user about local mode when fixes will be applied. - if (!onlyCompute && ghsas.length) { + if (!dontApplyFixes && ghsas.length) { const envCheck = checkCiEnvVars() if (envCheck.present.length) { // Some CI vars are set but not all - show what's missing. @@ -143,7 +143,7 @@ export async function coanaFix( ? ['--range-style', fixConfig.rangeStyle] : []), ...(glob ? ['--glob', glob] : []), - ...(onlyCompute ? [FLAG_DRY_RUN] : []), + ...(dontApplyFixes ? [FLAG_DRY_RUN] : []), ...(outputFile ? ['--output-file', outputFile] : []), ...fixConfig.unknownFlags, ], diff --git a/src/commands/fix/handle-fix.mts b/src/commands/fix/handle-fix.mts index f26e665a3..c37fb9e7e 100644 --- a/src/commands/fix/handle-fix.mts +++ b/src/commands/fix/handle-fix.mts @@ -16,12 +16,12 @@ const CVE_FORMAT_REGEXP = /^CVE-\d{4}-\d{4,}$/ export type HandleFixConfig = Remap< FixConfig & { + dontApplyFixes: boolean ghsas: string[] glob: string orgSlug: string outputKind: OutputKind unknownFlags: string[] - onlyCompute: boolean outputFile: string } > @@ -100,11 +100,11 @@ export async function convertIdsToGhsas(ids: string[]): Promise { export async function handleFix({ autopilot, cwd, + dontApplyFixes, ghsas, glob, limit, minSatisfying, - onlyCompute, orgSlug, outputFile, outputKind, @@ -121,7 +121,7 @@ export async function handleFix({ glob, limit, minSatisfying, - onlyCompute, + dontApplyFixes, outputFile, outputKind, prCheck, @@ -132,6 +132,7 @@ export async function handleFix({ await outputFixResult( await coanaFix({ autopilot, + dontApplyFixes, cwd, // Convert mixed CVE/GHSA/PURL inputs to GHSA IDs only ghsas: await convertIdsToGhsas(ghsas), @@ -143,7 +144,6 @@ export async function handleFix({ rangeStyle, spinner, unknownFlags, - onlyCompute, outputFile, }), outputKind, diff --git a/src/commands/fix/types.mts b/src/commands/fix/types.mts index 14ede6afb..a21b7bda8 100644 --- a/src/commands/fix/types.mts +++ b/src/commands/fix/types.mts @@ -3,6 +3,7 @@ import type { Spinner } from '@socketsecurity/registry/lib/spinner' export type FixConfig = { autopilot: boolean + dontApplyFixes: boolean cwd: string ghsas: string[] glob: string @@ -13,6 +14,5 @@ export type FixConfig = { rangeStyle: RangeStyle spinner: Spinner | undefined unknownFlags: string[] - onlyCompute: boolean outputFile: string } diff --git a/src/commands/optimize/cmd-optimize-pnpm-versions.test.mts b/src/commands/optimize/cmd-optimize-pnpm-versions.test.mts index aec894cf5..432a252ec 100644 --- a/src/commands/optimize/cmd-optimize-pnpm-versions.test.mts +++ b/src/commands/optimize/cmd-optimize-pnpm-versions.test.mts @@ -1,15 +1,7 @@ -import { existsSync, promises as fs } from 'node:fs' +import { existsSync } from 'node:fs' import path from 'node:path' -import { - afterAll, - afterEach, - beforeAll, - beforeEach, - describe, - expect, - it, -} from 'vitest' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { readPackageJson } from '@socketsecurity/registry/lib/packages' import { spawnSync } from '@socketsecurity/registry/lib/spawn' diff --git a/src/utils/extract-names.mts b/src/utils/extract-names.mts new file mode 100644 index 000000000..94fd4ce56 --- /dev/null +++ b/src/utils/extract-names.mts @@ -0,0 +1,55 @@ +import constants from '../constants.mts' + +/** + * Sanitizes a name to comply with repository naming constraints. + * Constraints: 100 or less A-Za-z0-9 characters only with non-repeating, + * non-leading or trailing ., _ or - only. + * + * @param name - The name to sanitize + * @returns Sanitized name that complies with repository naming rules, or empty string if no valid characters + */ +function sanitizeName(name: string): string { + if (!name) { + return '' + } + + // Replace sequences of illegal characters with underscores. + const sanitized = name + // Replace any sequence of non-alphanumeric characters (except ., _, -) with underscore. + .replace(/[^A-Za-z0-9._-]+/g, '_') + // Replace sequences of multiple allowed special chars with single underscore. + .replace(/[._-]{2,}/g, '_') + // Remove leading special characters. + .replace(/^[._-]+/, '') + // Remove trailing special characters. + .replace(/[._-]+$/, '') + // Truncate to 100 characters max. + .slice(0, 100) + + return sanitized +} + +/** + * Extracts and sanitizes a repository name. + * + * @param name - The repository name to extract and sanitize + * @returns Sanitized repository name, or default repository name if empty + */ +export function extractName(name: string): string { + const sanitized = sanitizeName(name) + return sanitized || constants.SOCKET_DEFAULT_REPOSITORY +} + +/** + * Extracts and sanitizes a repository owner name. + * + * @param owner - The repository owner name to extract and sanitize + * @returns Sanitized repository owner name, or undefined if input is empty + */ +export function extractOwner(owner: string): string | undefined { + if (!owner) { + return undefined + } + const sanitized = sanitizeName(owner) + return sanitized || undefined +} diff --git a/src/utils/extract-names.test.mts b/src/utils/extract-names.test.mts new file mode 100644 index 000000000..68ddae1c4 --- /dev/null +++ b/src/utils/extract-names.test.mts @@ -0,0 +1,120 @@ +import { describe, expect, it } from 'vitest' + +import constants from '../constants.mts' +import { extractName, extractOwner } from './extract-names.mts' + +describe('extractName', () => { + it('should return valid names unchanged', () => { + expect(extractName('myrepo')).toBe('myrepo') + expect(extractName('My-Repo_123')).toBe('My-Repo_123') + expect(extractName('repo.with.dots')).toBe('repo.with.dots') + expect(extractName('a1b2c3')).toBe('a1b2c3') + }) + + it('should replace sequences of illegal characters with underscore', () => { + expect(extractName('repo@#$%name')).toBe('repo_name') + expect(extractName('repo name')).toBe('repo_name') + expect(extractName('repo!!!name')).toBe('repo_name') + expect(extractName('repo/\\|name')).toBe('repo_name') + }) + + it('should replace sequences of multiple allowed special chars with single underscore', () => { + expect(extractName('repo...name')).toBe('repo_name') + expect(extractName('repo---name')).toBe('repo_name') + expect(extractName('repo___name')).toBe('repo_name') + expect(extractName('repo.-_name')).toBe('repo_name') + }) + + it('should remove leading special characters', () => { + expect(extractName('...repo')).toBe('repo') + expect(extractName('---repo')).toBe('repo') + expect(extractName('___repo')).toBe('repo') + expect(extractName('.-_repo')).toBe('repo') + }) + + it('should remove trailing special characters', () => { + expect(extractName('repo...')).toBe('repo') + expect(extractName('repo---')).toBe('repo') + expect(extractName('repo___')).toBe('repo') + expect(extractName('repo.-_')).toBe('repo') + }) + + it('should truncate names longer than 100 characters', () => { + const longName = 'a'.repeat(150) + expect(extractName(longName)).toBe('a'.repeat(100)) + }) + + it('should handle combined transformations', () => { + expect(extractName('---repo@#$name...')).toBe('repo_name') + expect(extractName(' ...my/repo\\name___ ')).toBe('my_repo_name') + }) + + it('should return default repository name for empty or invalid inputs', () => { + expect(extractName('')).toBe(constants.SOCKET_DEFAULT_REPOSITORY) + expect(extractName('...')).toBe(constants.SOCKET_DEFAULT_REPOSITORY) + expect(extractName('___')).toBe(constants.SOCKET_DEFAULT_REPOSITORY) + expect(extractName('---')).toBe(constants.SOCKET_DEFAULT_REPOSITORY) + expect(extractName('@#$%')).toBe(constants.SOCKET_DEFAULT_REPOSITORY) + }) +}) + +describe('extractOwner', () => { + it('should return valid owner names unchanged', () => { + expect(extractOwner('myowner')).toBe('myowner') + expect(extractOwner('My-Owner_123')).toBe('My-Owner_123') + expect(extractOwner('owner.with.dots')).toBe('owner.with.dots') + expect(extractOwner('a1b2c3')).toBe('a1b2c3') + }) + + it('should replace sequences of illegal characters with underscore', () => { + expect(extractOwner('owner@#$%name')).toBe('owner_name') + expect(extractOwner('owner name')).toBe('owner_name') + expect(extractOwner('owner!!!name')).toBe('owner_name') + expect(extractOwner('owner/\\|name')).toBe('owner_name') + }) + + it('should replace sequences of multiple allowed special chars with single underscore', () => { + expect(extractOwner('owner...name')).toBe('owner_name') + expect(extractOwner('owner---name')).toBe('owner_name') + expect(extractOwner('owner___name')).toBe('owner_name') + expect(extractOwner('owner.-_name')).toBe('owner_name') + }) + + it('should remove leading special characters', () => { + expect(extractOwner('...owner')).toBe('owner') + expect(extractOwner('---owner')).toBe('owner') + expect(extractOwner('___owner')).toBe('owner') + expect(extractOwner('.-_owner')).toBe('owner') + }) + + it('should remove trailing special characters', () => { + expect(extractOwner('owner...')).toBe('owner') + expect(extractOwner('owner---')).toBe('owner') + expect(extractOwner('owner___')).toBe('owner') + expect(extractOwner('owner.-_')).toBe('owner') + }) + + it('should truncate names longer than 100 characters', () => { + const longName = 'a'.repeat(150) + expect(extractOwner(longName)).toBe('a'.repeat(100)) + }) + + it('should handle combined transformations', () => { + expect(extractOwner('---owner@#$name...')).toBe('owner_name') + expect(extractOwner(' ...my/owner\\name___ ')).toBe('my_owner_name') + }) + + it('should return undefined for empty or invalid inputs', () => { + expect(extractOwner('')).toBeUndefined() + expect(extractOwner('...')).toBeUndefined() + expect(extractOwner('___')).toBeUndefined() + expect(extractOwner('---')).toBeUndefined() + expect(extractOwner('@#$%')).toBeUndefined() + }) + + it('should handle edge cases with mixed valid and invalid characters', () => { + expect(extractOwner('a@b#c$d')).toBe('a_b_c_d') + expect(extractOwner('123...456')).toBe('123_456') + expect(extractOwner('---a---')).toBe('a') + }) +}) diff --git a/src/utils/git.mts b/src/utils/git.mts index f27411262..c205e1716 100644 --- a/src/utils/git.mts +++ b/src/utils/git.mts @@ -31,6 +31,7 @@ import { isSpawnError, spawn } from '@socketsecurity/registry/lib/spawn' import constants, { FLAG_QUIET } from '../constants.mts' import { debugGit } from './debug.mts' +import { extractName, extractOwner } from './extract-names.mts' import type { CResult } from '../types.mts' import type { SpawnOptions } from '@socketsecurity/registry/lib/spawn' @@ -103,14 +104,16 @@ export async function getRepoInfo( export async function getRepoName(cwd = process.cwd()): Promise { const repoInfo = await getRepoInfo(cwd) - return repoInfo?.repo ?? constants.SOCKET_DEFAULT_REPOSITORY + return repoInfo?.repo + ? extractName(repoInfo.repo) + : constants.SOCKET_DEFAULT_REPOSITORY } export async function getRepoOwner( cwd = process.cwd(), ): Promise { const repoInfo = await getRepoInfo(cwd) - return repoInfo?.owner + return repoInfo?.owner ? extractOwner(repoInfo.owner) : undefined } export async function gitBranch(