init attest action

This commit is contained in:
ejahnGithub
2024-02-22 07:53:51 -08:00
parent d9dd4e3410
commit e3c685d193
48 changed files with 85876 additions and 1630 deletions
+157 -12
View File
@@ -1,26 +1,171 @@
import { Attestation, Predicate, Subject, attest } from '@actions/attest'
import * as core from '@actions/core'
import { wait } from './wait'
import * as github from '@actions/github'
import { BUNDLE_V02_MEDIA_TYPE } from '@sigstore/bundle'
import { attachArtifactToImage, getRegistryCredentials } from '@sigstore/oci'
import fs from 'fs'
import os from 'os'
import path from 'path'
import {
FULCIO_INTERNAL_URL,
FULCIO_PUBLIC_GOOD_URL,
REKOR_PUBLIC_GOOD_URL,
SEARCH_PUBLIC_GOOD_URL,
TSA_INTERNAL_URL
} from './helper/endpoints'
import { predicateFromInputs } from './helper/predicate'
import { subjectFromInputs } from './helper/subject'
type Endpoints = {
fulcioURL: string
rekorURL?: string
tsaServerURL?: string
}
const COLOR_CYAN = '\x1B[36m'
const COLOR_DEFAULT = '\x1B[39m'
const ATTESTATION_FILE_NAME = 'attestation.jsonl'
const SIGSTORE_PUBLIC_GOOD_ENDPOINTS: Endpoints = {
fulcioURL: FULCIO_PUBLIC_GOOD_URL,
rekorURL: REKOR_PUBLIC_GOOD_URL
}
const SIGSTORE_INTERNAL_ENDPOINTS: Endpoints = {
fulcioURL: FULCIO_INTERNAL_URL,
tsaServerURL: TSA_INTERNAL_URL
}
/**
* The main function for the action.
* @returns {Promise<void>} Resolves when the action is complete.
*/
export async function run(): Promise<void> {
// Provenance visibility will be public ONLY if we can confirm that the
// repository is public AND the undocumented "private-signing" arg is NOT set.
// Otherwise, it will be private.
const endpoints =
github.context.payload.repository?.visibility === 'public' &&
core.getInput('private-signing') !== 'true'
? SIGSTORE_PUBLIC_GOOD_ENDPOINTS
: SIGSTORE_INTERNAL_ENDPOINTS
try {
const ms: string = core.getInput('milliseconds')
// Calculate subject from inputs and generate provenance
const subjects = await subjectFromInputs()
const predicate = predicateFromInputs()
const outputPath = path.join(tempDir(), ATTESTATION_FILE_NAME)
// Debug logs are only output if the `ACTIONS_STEP_DEBUG` secret is true
core.debug(`Waiting ${ms} milliseconds ...`)
// Generate attestations for each subject serially
for (const subject of subjects) {
const att = await createAttestation(subject, predicate, endpoints)
// Log the current timestamp, wait, then log the new timestamp
core.debug(new Date().toTimeString())
await wait(parseInt(ms, 10))
core.debug(new Date().toTimeString())
// Write attestation bundle to output file
fs.writeFileSync(outputPath, JSON.stringify(att.bundle) + os.EOL, {
encoding: 'utf-8',
flag: 'a'
})
// Set outputs for other workflow steps to use
core.setOutput('time', new Date().toTimeString())
} catch (error) {
if (att.attestationID) {
core.summary.addLink(
`${subject.name}@${subjectDigest(subject)}`,
attestationURL(att.attestationID)
)
}
}
if (!core.summary.isEmptyBuffer()) {
core.summary.addHeading('Attestation(s) Created', 3)
core.summary.write()
}
core.setOutput('bundle-path', outputPath)
} catch (err) {
// Fail the workflow run if an error occurs
if (error instanceof Error) core.setFailed(error.message)
core.setFailed(
err instanceof Error ? err.message : /* istanbul ignore next */ `${err}`
)
/* istanbul ignore if */
if (err instanceof Error && 'cause' in err) {
const innerErr = err.cause
core.debug(innerErr instanceof Error ? innerErr.message : `${innerErr}}`)
}
}
}
const createAttestation = async (
subject: Subject,
predicate: Predicate,
endpoints: Endpoints
): Promise<Attestation> => {
// Sign provenance w/ Sigstore
const attestation = await attest({
...endpoints,
subjectName: subject.name,
subjectDigest: subject.digest,
predicateType: predicate.type,
predicate: predicate.params,
token: core.getInput('github-token')
})
core.startGroup(
highlight(
`Attestation signed using ephemeral certificate from ${endpoints.fulcioURL}`
)
)
core.info(attestation.certificate)
core.endGroup()
if (attestation.tlogID) {
core.info(
highlight('Attestation signature uploaded to Rekor transparency log')
)
core.info(`${SEARCH_PUBLIC_GOOD_URL}?logIndex=${attestation.tlogID}`)
}
if (attestation.attestationID) {
core.info(highlight('Attestation uploaded to repository'))
core.info(attestationURL(attestation.attestationID))
}
if (core.getBooleanInput('push-to-registry', { required: false })) {
const credentials = getRegistryCredentials(subject.name)
const artifact = await attachArtifactToImage({
credentials,
imageName: subject.name,
imageDigest: subjectDigest(subject),
artifact: Buffer.from(JSON.stringify(attestation.bundle)),
mediaType: BUNDLE_V02_MEDIA_TYPE,
annotations: {
'dev.sigstore.bundle/predicateType': core.getInput('predicate-type')
}
})
core.info(highlight('Attestation uploaded to registry'))
core.info(`${subject.name}@${artifact.digest}`)
}
return attestation
}
const highlight = (str: string): string => `${COLOR_CYAN}${str}${COLOR_DEFAULT}`
const tempDir = (): string => {
const basePath = process.env['RUNNER_TEMP']
if (!basePath) {
throw new Error('Missing RUNNER_TEMP environment variable')
}
return fs.mkdtempSync(path.join(basePath, path.sep))
}
// Returns the subject's digest as a formatted string of the form
// "<algorithm>:<digest>".
const subjectDigest = (subject: Subject): string => {
const alg = Object.keys(subject.digest).sort()[0]
return `${alg}:${subject.digest[alg]}`
}
const attestationURL = (id: string): string =>
`${github.context.serverUrl}/${github.context.repo.owner}/${github.context.repo.repo}/attestations/${id}`