Files
hermes-agent/apps/desktop/scripts/notarize-artifact.cjs
T
Brooklyn Nicholson 2ce691d8ca fix(desktop): address CodeQL alerts on PR #20059
- settings/helpers.ts: harden setNested against prototype pollution.
  POLLUTING_PATH_PARTS check is now applied at every assignment site
  (loop + leaf) and uses Object.defineProperty so CodeQL can see the
  guard inline rather than via a helper function call.

- lib/markdown-preprocess.ts: rebuild the dangling-fence close regex
  from a fence-char + length instead of marker.replace(...). The marker
  is captured by `(`{3,}|~{3,})` so it can only be backticks or tildes,
  but CodeQL was tracing tainted input text into the RegExp source and
  flagging hostname dots from input as part of the pattern (false
  positive js/incomplete-hostname-regexp on the test fixture URLs).
  Reconstructing from a literal char breaks the dataflow.

- scripts/notarize-artifact.cjs: drop args from the run() rejection
  message. Args carry --key-id / --issuer / key file path; the existing
  outer catch already squashes errors to a generic line, but CodeQL was
  flagging the args.join(' ') as clear-text logging of APPLE_API_KEY_ID.

Composer DOM-text-as-HTML alerts (composer/index.tsx:379, :547) are
already addressed in 4dd9732a9 — innerHTML assignment was replaced with
renderComposerContents which builds DOM via replaceChildren / append
text nodes (no HTML interpretation).
2026-05-11 16:52:32 -04:00

78 lines
2.5 KiB
JavaScript

const fs = require('node:fs')
const os = require('node:os')
const path = require('node:path')
const { execFile } = require('node:child_process')
function run(command, args) {
return new Promise((resolve, reject) => {
execFile(command, args, (error, stdout, stderr) => {
if (error) {
// Intentionally omit args from the rejection message: callers pass
// notarization credentials (key id, issuer, key file path) here, and
// surfacing them in error output would land in CI logs.
reject(new Error(`${command} failed: ${stderr?.trim() || stdout?.trim() || error.message}`))
return
}
resolve()
})
})
}
function inlineKeyLooksValid(value) {
return value.includes('BEGIN PRIVATE KEY') && value.includes('END PRIVATE KEY')
}
function resolveApiKeyPath(rawValue) {
const value = String(rawValue || '').trim()
if (!value) return { keyPath: '', cleanup: () => {} }
if (fs.existsSync(value)) {
return { keyPath: value, cleanup: () => {} }
}
if (!inlineKeyLooksValid(value)) {
throw new Error('APPLE_API_KEY must be a file path or inline .p8 key content')
}
const tempPath = path.join(os.tmpdir(), `hermes-notary-${Date.now()}-${process.pid}.p8`)
fs.writeFileSync(tempPath, value, 'utf8')
return {
keyPath: tempPath,
cleanup: () => fs.rmSync(tempPath, { force: true })
}
}
async function main() {
const artifactPath = process.argv[2]
if (!artifactPath || !fs.existsSync(artifactPath)) {
throw new Error(`Missing artifact to notarize: ${artifactPath || '(none)'}`)
}
const profile = String(process.env.APPLE_NOTARY_PROFILE || '').trim()
if (profile) {
await run('xcrun', ['notarytool', 'submit', artifactPath, '--keychain-profile', profile, '--wait'])
await run('xcrun', ['stapler', 'staple', '-v', artifactPath])
return
}
const keyId = String(process.env.APPLE_API_KEY_ID || '').trim()
const issuer = String(process.env.APPLE_API_ISSUER || '').trim()
const rawApiKey = process.env.APPLE_API_KEY
if (!rawApiKey || !keyId || !issuer) {
throw new Error('APPLE_API_KEY, APPLE_API_KEY_ID, and APPLE_API_ISSUER are required')
}
const { keyPath, cleanup } = resolveApiKeyPath(rawApiKey)
try {
await run('xcrun', ['notarytool', 'submit', artifactPath, '--key', keyPath, '--key-id', keyId, '--issuer', issuer, '--wait'])
await run('xcrun', ['stapler', 'staple', '-v', artifactPath])
} finally {
cleanup()
}
}
main().catch(() => {
console.error('Notarization failed. Check configuration and command output in secure CI logs.')
process.exit(1)
})