mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-17 03:56:45 +00:00
Add SHA-256 checksum verification to menubar installer
The installer now downloads and verifies a .sha256 companion file before extracting and launching the menubar app. Build script and CI workflow generate the checksum alongside the zip. Adds SECURITY.md with reporting instructions. Addresses #215.
This commit is contained in:
parent
cf8c2aa493
commit
15334fac67
4 changed files with 73 additions and 11 deletions
4
.github/workflows/release-menubar.yml
vendored
4
.github/workflows/release-menubar.yml
vendored
|
|
@ -65,5 +65,7 @@ jobs:
|
|||
quarantine, and launches it. If you download the zip from this page directly
|
||||
and macOS shows "cannot verify developer", right-click the app in Finder and
|
||||
pick Open to whitelist it once.
|
||||
files: mac/.build/dist/CodeBurnMenubar-*.zip
|
||||
files: |
|
||||
mac/.build/dist/CodeBurnMenubar-*.zip
|
||||
mac/.build/dist/CodeBurnMenubar-*.zip.sha256
|
||||
fail_on_unmatched_files: true
|
||||
|
|
|
|||
21
SECURITY.md
Normal file
21
SECURITY.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Please report security vulnerabilities via [GitHub's private vulnerability reporting](https://github.com/getagentseal/codeburn/security/advisories/new).
|
||||
|
||||
Do not open a public issue for security vulnerabilities.
|
||||
|
||||
## Scope
|
||||
|
||||
Security reports are welcome for:
|
||||
|
||||
- The CLI (`src/`)
|
||||
- The menubar installer (`src/menubar-installer.ts`)
|
||||
- The macOS menubar app (`mac/`)
|
||||
- The desktop app (`desktop/`)
|
||||
- CI/CD workflows (`.github/workflows/`)
|
||||
|
||||
## Release Integrity
|
||||
|
||||
Menubar release assets include a `.sha256` checksum file. The installer verifies the checksum before extracting and launching the downloaded bundle.
|
||||
|
|
@ -98,6 +98,13 @@ ZIP_PATH="${DIST_DIR}/${ZIP_NAME}"
|
|||
echo "▸ Packaging ${ZIP_NAME}..."
|
||||
(cd "${DIST_DIR}" && /usr/bin/ditto -c -k --keepParent "${BUNDLE_NAME}" "${ZIP_NAME}")
|
||||
|
||||
CHECKSUM_NAME="${ZIP_NAME}.sha256"
|
||||
CHECKSUM_PATH="${DIST_DIR}/${CHECKSUM_NAME}"
|
||||
echo "▸ Computing SHA-256 checksum..."
|
||||
(cd "${DIST_DIR}" && shasum -a 256 "${ZIP_NAME}" > "${CHECKSUM_NAME}")
|
||||
|
||||
echo ""
|
||||
echo "✓ Built ${ZIP_PATH}"
|
||||
echo "✓ Checksum ${CHECKSUM_PATH}"
|
||||
cat "${CHECKSUM_PATH}"
|
||||
ls -la "${DIST_DIR}"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { spawn } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { createWriteStream } from 'node:fs'
|
||||
import { mkdir, mkdtemp, rename, rm, stat } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, readFile, rename, rm, stat } from 'node:fs/promises'
|
||||
import { homedir, platform, tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
|
|
@ -11,6 +12,7 @@ import { Readable } from 'node:stream'
|
|||
const RELEASE_API = 'https://api.github.com/repos/getagentseal/codeburn/releases/latest'
|
||||
const APP_BUNDLE_NAME = 'CodeBurnMenubar.app'
|
||||
const ASSET_PATTERN = /^CodeBurnMenubar-.*\.zip$/
|
||||
const CHECKSUM_PATTERN = /^CodeBurnMenubar-.*\.zip\.sha256$/
|
||||
const APP_PROCESS_NAME = 'CodeBurnMenubar'
|
||||
const SUPPORTED_OS = 'darwin'
|
||||
const MIN_MACOS_MAJOR = 14
|
||||
|
|
@ -19,6 +21,7 @@ export type InstallResult = { installedPath: string; launched: boolean }
|
|||
|
||||
type ReleaseAsset = { name: string; browser_download_url: string }
|
||||
type ReleaseResponse = { tag_name: string; assets: ReleaseAsset[] }
|
||||
type ResolvedAssets = { zip: ReleaseAsset; checksum: ReleaseAsset | null }
|
||||
|
||||
function userApplicationsDir(): string {
|
||||
return join(homedir(), 'Applications')
|
||||
|
|
@ -57,10 +60,9 @@ async function sysProductVersion(): Promise<string> {
|
|||
})
|
||||
}
|
||||
|
||||
async function fetchLatestReleaseAsset(): Promise<ReleaseAsset> {
|
||||
async function fetchLatestReleaseAssets(): Promise<ResolvedAssets> {
|
||||
const response = await fetch(RELEASE_API, {
|
||||
headers: {
|
||||
// Identify the installer so GitHub's abuse heuristics treat us as a known client.
|
||||
'User-Agent': 'codeburn-menubar-installer',
|
||||
Accept: 'application/vnd.github+json',
|
||||
},
|
||||
|
|
@ -69,14 +71,37 @@ async function fetchLatestReleaseAsset(): Promise<ReleaseAsset> {
|
|||
throw new Error(`GitHub release lookup failed: HTTP ${response.status}`)
|
||||
}
|
||||
const body = await response.json() as ReleaseResponse
|
||||
const asset = body.assets.find(a => ASSET_PATTERN.test(a.name))
|
||||
if (!asset) {
|
||||
const zip = body.assets.find(a => ASSET_PATTERN.test(a.name))
|
||||
if (!zip) {
|
||||
throw new Error(
|
||||
`No ${APP_BUNDLE_NAME} zip found in release ${body.tag_name}. ` +
|
||||
`Check https://github.com/getagentseal/codeburn/releases.`
|
||||
)
|
||||
}
|
||||
return asset
|
||||
const checksum = body.assets.find(a => CHECKSUM_PATTERN.test(a.name)) ?? null
|
||||
return { zip, checksum }
|
||||
}
|
||||
|
||||
async function verifyChecksum(archivePath: string, checksumUrl: string): Promise<void> {
|
||||
const response = await fetch(checksumUrl, {
|
||||
headers: { 'User-Agent': 'codeburn-menubar-installer' },
|
||||
redirect: 'follow',
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Checksum download failed: HTTP ${response.status}`)
|
||||
}
|
||||
const text = await response.text()
|
||||
const expected = text.trim().split(/\s+/)[0]!.toLowerCase()
|
||||
const fileBytes = await readFile(archivePath)
|
||||
const actual = createHash('sha256').update(fileBytes).digest('hex')
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`Checksum mismatch for ${archivePath}.\n` +
|
||||
` Expected: ${expected}\n` +
|
||||
` Got: ${actual}\n` +
|
||||
`The download may be corrupted or tampered with.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadToFile(url: string, destPath: string): Promise<void> {
|
||||
|
|
@ -134,13 +159,20 @@ export async function installMenubarApp(options: { force?: boolean } = {}): Prom
|
|||
}
|
||||
|
||||
console.log('Looking up the latest CodeBurn Menubar release...')
|
||||
const asset = await fetchLatestReleaseAsset()
|
||||
const { zip, checksum } = await fetchLatestReleaseAssets()
|
||||
|
||||
const stagingDir = await mkdtemp(join(tmpdir(), 'codeburn-menubar-'))
|
||||
try {
|
||||
const archivePath = join(stagingDir, asset.name)
|
||||
console.log(`Downloading ${asset.name}...`)
|
||||
await downloadToFile(asset.browser_download_url, archivePath)
|
||||
const archivePath = join(stagingDir, zip.name)
|
||||
console.log(`Downloading ${zip.name}...`)
|
||||
await downloadToFile(zip.browser_download_url, archivePath)
|
||||
|
||||
if (checksum) {
|
||||
console.log('Verifying checksum...')
|
||||
await verifyChecksum(archivePath, checksum.browser_download_url)
|
||||
} else {
|
||||
console.log('Warning: no checksum file found in release, skipping verification.')
|
||||
}
|
||||
|
||||
console.log('Unpacking...')
|
||||
await runCommand('/usr/bin/unzip', ['-q', archivePath, '-d', stagingDir])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue