diff --git a/.claude/hooks/setup-security-tools/README.md b/.claude/hooks/setup-security-tools/README.md index 96c301596..02fde4a7c 100644 --- a/.claude/hooks/setup-security-tools/README.md +++ b/.claude/hooks/setup-security-tools/README.md @@ -12,7 +12,7 @@ Scans your Claude Code configuration (`.claude/` directory) for security issues ### 2. Zizmor Static analysis tool for GitHub Actions workflows. Catches unpinned actions, secret exposure, template injection, and permission issues. -**How it's installed**: Binary downloaded from [GitHub releases](https://github.com/woodruffw/zizmor/releases), SHA-256 verified, cached at `~/.socket/zizmor/bin/zizmor`. If you already have it via `brew install zizmor`, the download is skipped. +**How it's installed**: Binary downloaded from [GitHub releases](https://github.com/zizmorcore/zizmor/releases), SHA-256 verified, cached via the dlx system at `~/.socket/_dlx/`. If you already have it via `brew install zizmor`, the download is skipped. ### 3. SFW (Socket Firewall) Intercepts package manager commands (`npm install`, `pnpm add`, etc.) and scans packages against Socket.dev's malware database before installation. @@ -34,7 +34,7 @@ Claude will ask if you have an API key, then run the setup script. | Tool | Location | Persists across repos? | |------|----------|----------------------| | AgentShield | `node_modules/.bin/agentshield` | No (per-repo devDep) | -| Zizmor | `~/.socket/zizmor/bin/zizmor` | Yes | +| Zizmor | `~/.socket/_dlx//zizmor` | Yes | | SFW binary | `~/.socket/_dlx//sfw` | Yes | | SFW shims | `~/.socket/sfw/shims/npm`, etc. | Yes | @@ -66,7 +66,7 @@ Self-contained. To add to another Socket repo: **"AgentShield not found"** — Run `pnpm install`. It's the `ecc-agentshield` devDependency. -**"zizmor found but wrong version"** — The script downloads the expected version to `~/.socket/zizmor/bin/`. Your system version (e.g. from brew) will be ignored in favor of the correct version. +**"zizmor found but wrong version"** — The script downloads the expected version via the dlx cache. Your system version (e.g. from brew) will be ignored in favor of the correct version. **"No supported package managers found"** — SFW only creates shims for package managers found on your PATH. Install npm/pnpm/etc. first. diff --git a/.claude/hooks/setup-security-tools/external-tools.json b/.claude/hooks/setup-security-tools/external-tools.json index 95482e5a7..c522483b8 100644 --- a/.claude/hooks/setup-security-tools/external-tools.json +++ b/.claude/hooks/setup-security-tools/external-tools.json @@ -4,7 +4,7 @@ "zizmor": { "description": "GitHub Actions security scanner", "version": "1.23.1", - "repository": "woodruffw/zizmor", + "repository": "zizmorcore/zizmor", "assets": { "darwin-arm64": "zizmor-aarch64-apple-darwin.tar.gz", "darwin-x64": "zizmor-x86_64-apple-darwin.tar.gz", diff --git a/.claude/hooks/setup-security-tools/index.mts b/.claude/hooks/setup-security-tools/index.mts index 97ef9e07c..b62d36522 100644 --- a/.claude/hooks/setup-security-tools/index.mts +++ b/.claude/hooks/setup-security-tools/index.mts @@ -5,7 +5,7 @@ // 1. AgentShield — scans Claude AI config for prompt injection / secrets. // Already a devDep (ecc-agentshield); this script verifies it's installed. // 2. Zizmor — static analysis for GitHub Actions workflows. Downloads the -// correct binary, verifies SHA-256, caches at ~/.socket/zizmor/bin/zizmor. +// correct binary, verifies SHA-256, cached via the dlx system. // 3. SFW (Socket Firewall) — intercepts package manager commands to scan // for malware. Downloads binary, verifies SHA-256, creates PATH shims. // Enterprise vs free determined by SOCKET_API_KEY in env / .env / .env.local. @@ -18,7 +18,6 @@ import { fileURLToPath } from 'node:url' import { whichSync } from '@socketsecurity/lib/bin' import { downloadBinary } from '@socketsecurity/lib/dlx/binary' -import { httpDownload } from '@socketsecurity/lib/http-request' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { getSocketHomePath } from '@socketsecurity/lib/paths/socket' import { spawn, spawnSync } from '@socketsecurity/lib/spawn' @@ -121,53 +120,50 @@ async function setupZizmor(): Promise { logger.log(`Found on PATH but wrong version (need v${ZIZMOR.version})`) } - // Check cached binary. - const ext = process.platform === 'win32' ? '.exe' : '' - const binDir = path.join(getSocketHomePath(), 'zizmor', 'bin') - const binPath = path.join(binDir, `zizmor${ext}`) - if (existsSync(binPath) && await checkZizmorVersion(binPath)) { - logger.log(`Cached: ${binPath} (v${ZIZMOR.version})`) - return true - } - - // Download. + // Download archive via dlx (handles caching + checksum). const platformKey = `${process.platform}-${process.arch}` const asset = ZIZMOR.assets?.[platformKey] if (!asset) throw new Error(`Unsupported platform: ${platformKey}`) const expectedSha = ZIZMOR.checksums?.[asset] if (!expectedSha) throw new Error(`No checksum for: ${asset}`) const url = `https://github.com/${ZIZMOR.repository}/releases/download/v${ZIZMOR.version}/${asset}` - const isZip = asset.endsWith('.zip') logger.log(`Downloading zizmor v${ZIZMOR.version} (${asset})...`) - const tmpFile = path.join(tmpdir(), `zizmor-${Date.now()}-${asset}`) - try { - await httpDownload(url, tmpFile, { sha256: expectedSha }) - logger.log('Download complete, checksum verified.') + const { binaryPath: archivePath, downloaded } = await downloadBinary({ + url, + name: `zizmor-${ZIZMOR.version}-${asset}`, + sha256: expectedSha, + }) + logger.log(downloaded ? 'Download complete, checksum verified.' : `Using cached archive: ${archivePath}`) + + // Extract binary from the cached archive. + const ext = process.platform === 'win32' ? '.exe' : '' + const binPath = path.join(path.dirname(archivePath), `zizmor${ext}`) + if (existsSync(binPath) && await checkZizmorVersion(binPath)) { + logger.log(`Cached: ${binPath} (v${ZIZMOR.version})`) + return true + } - // Extract. - const extractDir = path.join(tmpdir(), `zizmor-extract-${Date.now()}`) - await fs.mkdir(extractDir, { recursive: true }) + const isZip = asset.endsWith('.zip') + const extractDir = path.join(tmpdir(), `zizmor-extract-${Date.now()}`) + await fs.mkdir(extractDir, { recursive: true }) + try { if (isZip) { await spawn('powershell', ['-NoProfile', '-Command', - `Expand-Archive -Path '${tmpFile}' -DestinationPath '${extractDir}' -Force`], { stdio: 'pipe' }) + `Expand-Archive -Path '${archivePath}' -DestinationPath '${extractDir}' -Force`], { stdio: 'pipe' }) } else { - await spawn('tar', ['xzf', tmpFile, '-C', extractDir], { stdio: 'pipe' }) + await spawn('tar', ['xzf', archivePath, '-C', extractDir], { stdio: 'pipe' }) } - - // Install. const extractedBin = path.join(extractDir, `zizmor${ext}`) if (!existsSync(extractedBin)) throw new Error(`Binary not found after extraction: ${extractedBin}`) - await fs.mkdir(binDir, { recursive: true }) await fs.copyFile(extractedBin, binPath) await fs.chmod(binPath, 0o755) - await fs.rm(extractDir, { recursive: true, force: true }) - - logger.log(`Installed to ${binPath}`) - return true } finally { - if (existsSync(tmpFile)) await fs.unlink(tmpFile).catch(() => {}) + await fs.rm(extractDir, { recursive: true, force: true }).catch(() => {}) } + + logger.log(`Installed to ${binPath}`) + return true } // ── SFW ── diff --git a/.claude/hooks/setup-security-tools/update.mts b/.claude/hooks/setup-security-tools/update.mts index c174a77df..e25174156 100644 --- a/.claude/hooks/setup-security-tools/update.mts +++ b/.claude/hooks/setup-security-tools/update.mts @@ -5,7 +5,7 @@ // minimumReleaseAge cooldown (read from pnpm-workspace.yaml) for third-party tools. // Socket-owned tools (sfw) are excluded from cooldown. // -// Updates embedded checksums in index.mts when new versions are found. +// Updates external-tools.json when new versions or checksums are found. import { createHash } from 'node:crypto' import { existsSync, readFileSync, promises as fs } from 'node:fs' @@ -19,9 +19,8 @@ import { spawn } from '@socketsecurity/lib/spawn' const logger = getDefaultLogger() -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) -const INDEX_FILE = path.join(__dirname, 'index.mts') +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const CONFIG_FILE = path.join(__dirname, 'external-tools.json') const MS_PER_MINUTE = 60_000 const DEFAULT_COOLDOWN_MINUTES = 10_080 @@ -87,6 +86,31 @@ function versionFromTag(tag: string): string { return tag.replace(/^v/, '') } +// ── Config file I/O ── + +interface ToolConfig { + description?: string + version: string + repository?: string + assets?: Record + platforms?: Record + checksums?: Record + ecosystems?: string[] +} + +interface Config { + description?: string + tools: Record +} + +function readConfig(): Config { + return JSON.parse(readFileSync(CONFIG_FILE, 'utf8')) as Config +} + +async function writeConfig(config: Config): Promise { + await fs.writeFile(CONFIG_FILE, JSON.stringify(config, undefined, 2) + '\n', 'utf8') +} + // ── Checksum computation ── async function computeSha256(filePath: string): Promise { @@ -104,82 +128,6 @@ async function downloadAndHash(url: string): Promise { } } -// ── Index file manipulation ── - -function readIndexFile(): string { - return readFileSync(INDEX_FILE, 'utf8') -} - -async function writeIndexFile(content: string): Promise { - await fs.writeFile(INDEX_FILE, content, 'utf8') -} - -function replaceConstant( - source: string, - name: string, - oldValue: string, - newValue: string, -): string { - const escaped = oldValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - const pattern = new RegExp(`(const ${name}\\s*=\\s*')${escaped}'`) - return source.replace(pattern, `$1${newValue}'`) -} - -function replaceChecksumValue( - source: string, - assetName: string, - oldHash: string, - newHash: string, - objectName?: string, -): string { - // Match the specific asset line in a checksums object. - const escaped = assetName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - const multiLine = new RegExp( - `('${escaped}':\\s*\\n\\s*')${oldHash}'`, - ) - const singleLine = new RegExp( - `('${escaped}':\\s*')${oldHash}'`, - ) - // When objectName is provided, scope the replacement to that object block - // to avoid ambiguity when multiple objects share the same platform keys - // (e.g. SFW_FREE_CHECKSUMS and SFW_ENTERPRISE_CHECKSUMS both use 'linux-arm64'). - if (objectName) { - const objStart = source.indexOf(`const ${objectName}`) - if (objStart !== -1) { - const braceStart = source.indexOf('{', objStart) - if (braceStart !== -1) { - // Find the matching closing brace. - let depth = 0 - let braceEnd = -1 - for (let i = braceStart; i < source.length; i += 1) { - if (source[i] === '{') depth += 1 - else if (source[i] === '}') { - depth -= 1 - if (!depth) { - braceEnd = i + 1 - break - } - } - } - if (braceEnd !== -1) { - let block = source.slice(objStart, braceEnd) - if (multiLine.test(block)) { - block = block.replace(multiLine, `$1${newHash}'`) - } else { - block = block.replace(singleLine, `$1${newHash}'`) - } - return source.slice(0, objStart) + block + source.slice(braceEnd) - } - } - } - } - // Unscoped fallback: replace first match in entire source. - if (multiLine.test(source)) { - return source.replace(multiLine, `$1${newHash}'`) - } - return source.replace(singleLine, `$1${newHash}'`) -} - // ── Zizmor update ── interface UpdateResult { @@ -189,53 +137,34 @@ interface UpdateResult { updated: boolean } -// Map from index.mts asset names to zizmor release asset names. -const ZIZMOR_ASSETS: Record = { - __proto__: null as unknown as string, - 'zizmor-aarch64-apple-darwin.tar.gz': - 'zizmor-aarch64-apple-darwin.tar.gz', - 'zizmor-aarch64-unknown-linux-gnu.tar.gz': - 'zizmor-aarch64-unknown-linux-gnu.tar.gz', - 'zizmor-x86_64-apple-darwin.tar.gz': - 'zizmor-x86_64-apple-darwin.tar.gz', - 'zizmor-x86_64-pc-windows-msvc.zip': - 'zizmor-x86_64-pc-windows-msvc.zip', - 'zizmor-x86_64-unknown-linux-gnu.tar.gz': - 'zizmor-x86_64-unknown-linux-gnu.tar.gz', -} - -async function updateZizmor(source: string): Promise<{ - result: UpdateResult - source: string -}> { +async function updateZizmor(config: Config): Promise { const tool = 'zizmor' logger.log(`=== Checking ${tool} ===`) + const toolConfig = config.tools[tool] + if (!toolConfig) { + return { tool, skipped: true, updated: false, reason: 'not in config' } + } + + const repo = toolConfig.repository ?? 'zizmorcore/zizmor' + let release: GhRelease try { - release = await ghApiLatestRelease('woodruffw/zizmor') + release = await ghApiLatestRelease(repo) } catch (e) { const msg = e instanceof Error ? e.message : String(e) logger.warn(`Failed to fetch zizmor releases: ${msg}`) - return { - result: { tool, skipped: true, updated: false, reason: `API error: ${msg}` }, - source, - } + return { tool, skipped: true, updated: false, reason: `API error: ${msg}` } } const latestVersion = versionFromTag(release.tag_name) - // Extract current version from source. - const currentMatch = /const ZIZMOR_VERSION = '([^']+)'/.exec(source) - const currentVersion = currentMatch ? currentMatch[1] : '' + const currentVersion = toolConfig.version logger.log(`Current: v${currentVersion}, Latest: v${latestVersion}`) if (latestVersion === currentVersion) { logger.log('Already current.') - return { - result: { tool, skipped: false, updated: false, reason: 'already current' }, - source, - } + return { tool, skipped: false, updated: false, reason: 'already current' } } // Respect cooldown for third-party tools. @@ -243,10 +172,7 @@ async function updateZizmor(source: string): Promise<{ const daysOld = ((Date.now() - new Date(release.published_at).getTime()) / 86_400_000).toFixed(1) const cooldownDays = (COOLDOWN_MS / 86_400_000).toFixed(0) logger.log(`v${latestVersion} is only ${daysOld} days old (need ${cooldownDays}). Skipping.`) - return { - result: { tool, skipped: true, updated: false, reason: `too new (${daysOld} days, need ${cooldownDays})` }, - source, - } + return { tool, skipped: true, updated: false, reason: `too new (${daysOld} days, need ${cooldownDays})` } } logger.log(`Updating to v${latestVersion}...`) @@ -271,14 +197,16 @@ async function updateZizmor(source: string): Promise<{ } } - // Compute checksums for each platform asset. - let updated = source + // Compute checksums for each asset in the config. + const currentChecksums = toolConfig.checksums ?? {} + const newChecksums: Record = { __proto__: null } as unknown as Record let allFound = true - for (const assetName of Object.keys(ZIZMOR_ASSETS)) { + + for (const assetName of Object.keys(currentChecksums)) { let newHash: string | undefined // Try checksums.txt first. - if (checksumMap && checksumMap[assetName]) { + if (checksumMap?.[assetName]) { newHash = checksumMap[assetName] } else { // Download and compute. @@ -304,196 +232,111 @@ async function updateZizmor(source: string): Promise<{ continue } - // Find and replace the old hash. - const oldHashMatch = new RegExp( - `'${assetName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}':\\s*\\n\\s*'([a-f0-9]{64})'`, - ).exec(updated) - const oldHashSingle = new RegExp( - `'${assetName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}':\\s*'([a-f0-9]{64})'`, - ).exec(updated) - const oldHash = oldHashMatch?.[1] ?? oldHashSingle?.[1] + newChecksums[assetName] = newHash + const oldHash = currentChecksums[assetName] if (oldHash && oldHash !== newHash) { - updated = replaceChecksumValue(updated, assetName, oldHash, newHash) logger.log(` ${assetName}: ${oldHash.slice(0, 12)}... -> ${newHash.slice(0, 12)}...`) } else if (oldHash === newHash) { logger.log(` ${assetName}: unchanged`) - } else { - logger.warn(` ${assetName}: no existing checksum entry found in source`) - allFound = false } } if (!allFound) { logger.warn('Some assets could not be verified. Skipping version bump.') - return { - result: { tool, skipped: true, updated: false, reason: 'incomplete asset checksums' }, - source, - } + return { tool, skipped: true, updated: false, reason: 'incomplete asset checksums' } } - // Update version constant. - updated = replaceConstant(updated, 'ZIZMOR_VERSION', currentVersion!, latestVersion) - logger.log(`Updated ZIZMOR_VERSION: ${currentVersion} -> ${latestVersion}`) + // Update config. + toolConfig.version = latestVersion + toolConfig.checksums = newChecksums + logger.log(`Updated zizmor: ${currentVersion} -> ${latestVersion}`) - return { - result: { tool, skipped: false, updated: true, reason: `${currentVersion} -> ${latestVersion}` }, - source: updated, - } + return { tool, skipped: false, updated: true, reason: `${currentVersion} -> ${latestVersion}` } } // ── SFW update ── -const SFW_FREE_ASSET_NAMES: Record = { - __proto__: null as unknown as string, - 'linux-arm64': 'sfw-free-linux-arm64', - 'linux-x86_64': 'sfw-free-linux-x86_64', - 'macos-arm64': 'sfw-free-macos-arm64', - 'macos-x86_64': 'sfw-free-macos-x86_64', - 'windows-x86_64': 'sfw-free-windows-x86_64.exe', -} +async function updateSfwTool( + config: Config, + toolName: string, +): Promise { + const toolConfig = config.tools[toolName] + if (!toolConfig) { + return { tool: toolName, skipped: true, updated: false, reason: 'not in config' } + } -const SFW_ENTERPRISE_ASSET_NAMES: Record = { - __proto__: null as unknown as string, - 'linux-arm64': 'sfw-linux-arm64', - 'linux-x86_64': 'sfw-linux-x86_64', - 'macos-arm64': 'sfw-macos-arm64', - 'macos-x86_64': 'sfw-macos-x86_64', - 'windows-x86_64': 'sfw-windows-x86_64.exe', -} + const repo = toolConfig.repository + if (!repo) { + return { tool: toolName, skipped: true, updated: false, reason: 'no repository' } + } -async function fetchSfwChecksums( - repo: string, - label: string, - assetNames: Record, - currentChecksums: Record, -): Promise<{ - checksums: Record - changed: boolean -}> { let release: GhRelease try { release = await ghApiLatestRelease(repo) } catch (e) { const msg = e instanceof Error ? e.message : String(e) - logger.warn(`Failed to fetch ${label} releases: ${msg}`) - return { checksums: currentChecksums, changed: false } + logger.warn(`Failed to fetch ${toolName} releases: ${msg}`) + return { tool: toolName, skipped: true, updated: false, reason: `API error: ${msg}` } } - logger.log(` ${label}: latest ${release.tag_name} (published ${release.published_at.slice(0, 10)})`) + logger.log(` ${toolName}: latest ${release.tag_name} (published ${release.published_at.slice(0, 10)})`) + const currentChecksums = toolConfig.checksums ?? {} + const platforms = toolConfig.platforms ?? {} + const prefix = toolName === 'sfw-enterprise' ? 'sfw' : 'sfw-free' const newChecksums: Record = { __proto__: null } as unknown as Record let changed = false let allFound = true - for (const { 0: platform, 1: assetName } of Object.entries(assetNames)) { + for (const { 0: _, 1: sfwPlatform } of Object.entries(platforms)) { + const suffix = sfwPlatform.startsWith('windows') ? '.exe' : '' + const assetName = `${prefix}-${sfwPlatform}${suffix}` const asset = release.assets.find(a => a.name === assetName) const url = asset ? asset.browser_download_url - : `https://github.com/${repo}/releases/latest/download/${assetName}` + : `https://github.com/${repo}/releases/download/${release.tag_name}/${assetName}` logger.log(` Computing checksum for ${assetName}...`) try { const hash = await downloadAndHash(url) - newChecksums[platform] = hash - if (currentChecksums[platform] !== hash) { - logger.log(` ${platform}: ${(currentChecksums[platform] ?? '').slice(0, 12)}... -> ${hash.slice(0, 12)}...`) + newChecksums[sfwPlatform] = hash + if (currentChecksums[sfwPlatform] !== hash) { + logger.log(` ${sfwPlatform}: ${(currentChecksums[sfwPlatform] ?? '').slice(0, 12)}... -> ${hash.slice(0, 12)}...`) changed = true } } catch (e) { const msg = e instanceof Error ? e.message : String(e) logger.warn(` Failed to download ${assetName}: ${msg}`) - newChecksums[platform] = currentChecksums[platform] ?? '' allFound = false } } if (!allFound) { - logger.warn(` Some ${label} assets could not be downloaded. Skipping update.`) - return { checksums: currentChecksums, changed: false } + logger.warn(` Some ${toolName} assets could not be downloaded. Skipping update.`) + return { tool: toolName, skipped: true, updated: false, reason: 'incomplete downloads' } } - return { checksums: newChecksums, changed } -} - -function extractChecksums( - source: string, - objectName: string, -): Record { - const result: Record = { __proto__: null } as unknown as Record - // Find the object in source. - const objPattern = new RegExp( - `const ${objectName}[^{]*\\{[^}]*?(?:'([^']+)':\\s*'([a-f0-9]{64})'[,\\s]*)+`, - 's', - ) - const objMatch = objPattern.exec(source) - if (!objMatch) return result - - const block = objMatch[0] - const entryPattern = /'([^']+)':\s*\n?\s*'([a-f0-9]{64})'/g - let match: RegExpExecArray | null - while ((match = entryPattern.exec(block)) !== null) { - if (match[1] !== '__proto__') { - result[match[1]!] = match[2]! - } + if (changed) { + toolConfig.version = release.tag_name + toolConfig.checksums = newChecksums + return { tool: toolName, skipped: false, updated: true, reason: 'checksums updated' } } - return result + + return { tool: toolName, skipped: false, updated: false, reason: 'already current' } } -async function updateSfw(source: string): Promise<{ - results: UpdateResult[] - source: string -}> { +async function updateSfw(config: Config): Promise { logger.log('=== Checking SFW ===') - // Socket-owned tools: no cooldown. logger.log('Socket-owned tool: cooldown excluded.') const results: UpdateResult[] = [] - // Extract current checksums from source. - const currentFree = extractChecksums(source, 'SFW_FREE_CHECKSUMS') - const currentEnterprise = extractChecksums(source, 'SFW_ENTERPRISE_CHECKSUMS') - - // Check sfw-free. logger.log('') - const free = await fetchSfwChecksums( - 'SocketDev/sfw-free', - 'sfw-free', - SFW_FREE_ASSET_NAMES, - currentFree, - ) + results.push(await updateSfwTool(config, 'sfw-free')) - let updated = source - if (free.changed) { - for (const { 0: platform, 1: hash } of Object.entries(free.checksums)) { - if (currentFree[platform] && currentFree[platform] !== hash) { - updated = replaceChecksumValue(updated, platform, currentFree[platform]!, hash, 'SFW_FREE_CHECKSUMS') - } - } - results.push({ tool: 'sfw-free', skipped: false, updated: true, reason: 'checksums updated' }) - } else { - results.push({ tool: 'sfw-free', skipped: false, updated: false, reason: 'already current' }) - } - - // Check sfw enterprise. logger.log('') - const enterprise = await fetchSfwChecksums( - 'SocketDev/firewall-release', - 'sfw-enterprise', - SFW_ENTERPRISE_ASSET_NAMES, - currentEnterprise, - ) - - if (enterprise.changed) { - for (const { 0: platform, 1: hash } of Object.entries(enterprise.checksums)) { - if (currentEnterprise[platform] && currentEnterprise[platform] !== hash) { - updated = replaceChecksumValue(updated, platform, currentEnterprise[platform]!, hash, 'SFW_ENTERPRISE_CHECKSUMS') - } - } - results.push({ tool: 'sfw-enterprise', skipped: false, updated: true, reason: 'checksums updated' }) - } else { - results.push({ tool: 'sfw-enterprise', skipped: false, updated: false, reason: 'already current' }) - } + results.push(await updateSfwTool(config, 'sfw-enterprise')) - return { results, source: updated } + return results } // ── Main ── @@ -501,26 +344,23 @@ async function updateSfw(source: string): Promise<{ async function main(): Promise { logger.log('Checking for security tool updates...\n') - let source = readIndexFile() + const config = readConfig() const allResults: UpdateResult[] = [] // 1. Check zizmor (third-party, respects cooldown). - const zizmor = await updateZizmor(source) - source = zizmor.source - allResults.push(zizmor.result) + allResults.push(await updateZizmor(config)) logger.log('') // 2. Check sfw (Socket-owned, no cooldown). - const sfw = await updateSfw(source) - source = sfw.source - allResults.push(...sfw.results) + const sfwResults = await updateSfw(config) + allResults.push(...sfwResults) logger.log('') - // Write updated index.mts if anything changed. + // Write updated config if anything changed. const anyUpdated = allResults.some(r => r.updated) if (anyUpdated) { - await writeIndexFile(source) - logger.log('Updated index.mts with new checksums.\n') + await writeConfig(config) + logger.log('Updated external-tools.json.\n') } // Report.