Compare commits

...

27 Commits

Author SHA1 Message Date
Rob Herley 56832696fc npm audit fix 2024-07-03 17:03:40 +00:00
Rob Herley 176b40a888 allow localhost hostnames for artifact checks 2024-07-03 16:55:53 +00:00
Brian DeHamer 361a115e53 Merge pull request #1759 from actions/bdehamer/rekor-409
config rekor to fetch on conflict
2024-06-12 12:25:06 -07:00
Brian DeHamer dddc440d56 config rekor to fetch on conflict
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2024-06-12 11:57:18 -07:00
Brian DeHamer 08d6f14ea8 Merge pull request #1745 from actions/bdehamer/attest-provenance
(@actions/attest) New GHA provenance build type
2024-06-12 11:45:37 -07:00
Brian DeHamer 73100a7f85 new GHA build provenance
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2024-06-05 14:54:34 -07:00
Brian DeHamer c6b487124a Merge pull request #1738 from actions/bdehamer/attest-1.3.0
(@actions/attest) prepare 1.3.0 release
2024-06-05 14:53:11 -07:00
Brian DeHamer 8735a7e2da prep 1.3.0 release of @actions/attest
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2024-05-21 13:11:37 -07:00
Fredrik Skogman d1df13e178 Merge pull request #1735 from kommendorkapten/dynamic-urls
Read the server url from the environment variable.
2024-05-21 07:35:07 +02:00
Fredrik Skogman d3d7736bae Fixed a spelling error 2024-05-20 07:57:44 +02:00
Fredrik Skogman 7d18e7aa0d PR feedback. Juse more JS idiomatic code 2024-05-20 07:52:36 +02:00
Fredrik Skogman e60694077d Read the server url from the environment variable.
Instead of having the urls hardcoded, read them from the environment.
I opted to read from the environment variable instead of the github context
because it would be easier to test.
2024-05-16 17:00:35 +02:00
Brian DeHamer ae38557bb0 Merge pull request #1730 from actions/bdehamer/attest-readme
Update @actions/attest README
2024-05-01 11:48:55 -07:00
Brian DeHamer abb586d71e add doc link in @actions/attest readme
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2024-05-01 11:30:45 -07:00
Brian DeHamer 81a73aba8b Merge pull request #1725 from actions/bdehamer/attest-retry-persist
(@actions/attest) retry request on failure to save attestation
2024-04-24 19:59:43 -07:00
Brian DeHamer 0e8fe8af62 retry request on failure to save attestation
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2024-04-24 15:07:39 -07:00
Bethany 29885a805e Merge pull request #1724 from actions/bethanyj28/update-unzip-stream
Use latest `unzip-stream` and `unzip.Extract`
2024-04-24 09:09:09 -04:00
bethanyj28 9eb3d3a673 lint 2024-04-23 16:10:57 -04:00
bethanyj28 6e642f628f lint 2024-04-23 16:06:02 -04:00
bethanyj28 0159bbe7f2 bump version 2024-04-23 16:03:52 -04:00
bethanyj28 476276bf98 use latest unzip-stream 2024-04-23 15:54:54 -04:00
Brian DeHamer d82fd09f99 Merge pull request #1714 from actions/bdehamer/attest-no-make-fetch-happen
(@actions/attest) remove dep on make-fetch-happen
2024-04-23 10:39:57 -07:00
Brian DeHamer 2961d73391 remove dep on make-fetch-happen
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2024-04-23 09:39:17 -07:00
Rob Herley eb1cb3649c Merge pull request #1721 from actions/robherley/retry-502-invalid-body
artifact client: retry on non-JSON response
2024-04-19 14:02:46 -04:00
Rob Herley b384fe17ba bump pkg version + release notes 2024-04-19 15:08:30 +00:00
Rob Herley ccb1df45d1 artifact client: retry on non-JSON response 2024-04-19 14:03:47 +00:00
eggyhead 5a736647a1 Merge pull request #1712 from actions/vmjoseph/update-archiver
Upgrading `upload-artifact` and `download-artifact` archiver package
2024-04-15 13:03:10 -07:00
24 changed files with 737 additions and 558 deletions
+10 -10
View File
@@ -5175,12 +5175,12 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true,
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@@ -6400,9 +6400,9 @@
"dev": true
},
"node_modules/ejs": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
"dev": true,
"dependencies": {
"jake": "^10.8.5"
@@ -7457,9 +7457,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
+18 -2
View File
@@ -1,5 +1,21 @@
# @actions/artifact Releases
### 2.1.8
- Allows `*.localhost` domains for hostname checks for local development.
### 2.1.7
- Update unzip-stream dependency and reverted to using `unzip.Extract()`
### 2.1.6
- Will retry on invalid request responses.
### 2.1.5
- Bumped `archiver` dependency to 7.0.1
### 2.1.4
- Adds info-level logging for zip extraction
@@ -11,9 +27,9 @@
### 2.1.2
- Updated the stream extract functionality to use `unzip.Parse()` instead of `unzip.Extract()` for greater control of unzipping artifacts
### 2.1.1
- Updated `isGhes` check to include `.ghe.com` and `.ghe.localhost` as accepted hosts
### 2.1.0
@@ -116,6 +116,54 @@ describe('artifact-http-client', () => {
expect(mockPost).toHaveBeenCalledTimes(2)
})
it('should retry if invalid body response', async () => {
const mockPost = jest
.fn(() => {
const msgSucceeded = new http.IncomingMessage(new net.Socket())
msgSucceeded.statusCode = 200
return {
message: msgSucceeded,
readBody: async () => {
return Promise.resolve(
`{"ok": true, "signedUploadUrl": "http://localhost:8080/upload"}`
)
}
}
})
.mockImplementationOnce(() => {
const msgFailed = new http.IncomingMessage(new net.Socket())
msgFailed.statusCode = 502
msgFailed.statusMessage = 'Bad Gateway'
return {
message: msgFailed,
readBody: async () => {
return Promise.resolve('💥')
}
}
})
const mockHttpClient = (
HttpClient as unknown as jest.Mock
).mockImplementation(() => {
return {
post: mockPost
}
})
const client = internalArtifactTwirpClient(clientOptions)
const artifact = await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
version: 4
})
expect(mockHttpClient).toHaveBeenCalledTimes(1)
expect(artifact).toBeDefined()
expect(artifact.ok).toBe(true)
expect(artifact.signedUploadUrl).toBe('http://localhost:8080/upload')
expect(mockPost).toHaveBeenCalledTimes(2)
})
it('should fail if the request fails 5 times', async () => {
const mockPost = jest.fn(() => {
const msgFailed = new http.IncomingMessage(new net.Socket())
@@ -20,6 +20,11 @@ describe('isGhes', () => {
expect(config.isGhes()).toBe(false)
})
it('should return false when the request domain ends with .localhost', () => {
process.env.GITHUB_SERVER_URL = 'https://github.localhost'
expect(config.isGhes()).toBe(false)
})
it('should return false when the request domain is specific to an enterprise', () => {
process.env.GITHUB_SERVER_URL = 'https://my-enterprise.github.com'
expect(config.isGhes()).toBe(true)
@@ -200,14 +200,12 @@ describe('download-artifact', () => {
}
)
await expect(
downloadArtifactPublic(
fixtures.artifactID,
fixtures.repositoryOwner,
fixtures.repositoryName,
fixtures.token
)
).rejects.toBeInstanceOf(Error)
const response = await downloadArtifactPublic(
fixtures.artifactID,
fixtures.repositoryOwner,
fixtures.repositoryName,
fixtures.token
)
expect(downloadArtifactMock).toHaveBeenCalledWith({
owner: fixtures.repositoryOwner,
@@ -223,6 +221,16 @@ describe('download-artifact', () => {
expect(mockGetArtifactMalicious).toHaveBeenCalledWith(
fixtures.blobStorageUrl
)
// ensure path traversal was not possible
expect(
fs.existsSync(path.join(fixtures.workspaceDir, 'x/etc/hosts'))
).toBe(true)
expect(
fs.existsSync(path.join(fixtures.workspaceDir, 'y/etc/hosts'))
).toBe(true)
expect(response.downloadPath).toBe(fixtures.workspaceDir)
})
it('should successfully download an artifact to user defined path', async () => {
+5 -5
View File
@@ -1,12 +1,12 @@
{
"name": "@actions/artifact",
"version": "2.1.5",
"version": "2.1.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@actions/artifact",
"version": "2.1.5",
"version": "2.1.8",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.0",
@@ -1738,9 +1738,9 @@
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
},
"node_modules/unzip-stream": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.1.tgz",
"integrity": "sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==",
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.4.tgz",
"integrity": "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw==",
"dependencies": {
"binary": "^0.3.0",
"mkdirp": "^0.5.1"
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/artifact",
"version": "2.1.5",
"version": "2.1.8",
"preview": true,
"description": "Actions artifact lib",
"keywords": [
@@ -62,4 +62,4 @@
"typedoc-plugin-markdown": "^3.17.1",
"typescript": "^5.2.2"
}
}
}
@@ -1,7 +1,4 @@
import fs from 'fs/promises'
import * as stream from 'stream'
import {createWriteStream} from 'fs'
import * as path from 'path'
import * as github from '@actions/github'
import * as core from '@actions/core'
import * as httpClient from '@actions/http-client'
@@ -47,11 +44,6 @@ async function streamExtract(url: string, directory: string): Promise<void> {
await streamExtractExternal(url, directory)
return
} catch (error) {
if (error.message.includes('Malformed extraction path')) {
throw new Error(
`Artifact download failed with unretryable error: ${error.message}`
)
}
retryCount++
core.debug(
`Failed to download artifact after ${retryCount} retries due to ${error.message}. Retrying in 5 seconds...`
@@ -86,8 +78,6 @@ export async function streamExtractExternal(
}
const timer = setTimeout(timerFn, timeout)
const createdDirectories = new Set<string>()
createdDirectories.add(directory)
response.message
.on('data', () => {
timer.refresh()
@@ -99,46 +89,8 @@ export async function streamExtractExternal(
clearTimeout(timer)
reject(error)
})
.pipe(unzip.Parse())
.pipe(
new stream.Transform({
objectMode: true,
transform: async (entry, _, callback) => {
const fullPath = path.normalize(path.join(directory, entry.path))
if (!directory.endsWith(path.sep)) {
directory += path.sep
}
if (!fullPath.startsWith(directory)) {
reject(new Error(`Malformed extraction path: ${fullPath}`))
}
if (entry.type === 'Directory') {
if (!createdDirectories.has(fullPath)) {
createdDirectories.add(fullPath)
await resolveOrCreateDirectory(fullPath).then(() => {
entry.autodrain()
callback()
})
} else {
entry.autodrain()
callback()
}
} else {
core.info(`Extracting artifact entry: ${fullPath}`)
if (!createdDirectories.has(path.dirname(fullPath))) {
createdDirectories.add(path.dirname(fullPath))
await resolveOrCreateDirectory(path.dirname(fullPath))
}
const writeStream = createWriteStream(fullPath)
writeStream.on('finish', callback)
writeStream.on('error', reject)
entry.pipe(writeStream)
}
}
})
)
.on('finish', async () => {
.pipe(unzip.Extract({path: directory}))
.on('close', () => {
clearTimeout(timer)
resolve()
})
@@ -102,7 +102,6 @@ class ArtifactHttpClient implements Rpc {
} catch (error) {
if (error instanceof SyntaxError) {
debug(`Raw Body: ${rawBody}`)
throw error
}
if (error instanceof UsageError) {
@@ -30,10 +30,10 @@ export function isGhes(): boolean {
const hostname = ghUrl.hostname.trimEnd().toUpperCase()
const isGitHubHost = hostname === 'GITHUB.COM'
const isGheHost =
hostname.endsWith('.GHE.COM') || hostname.endsWith('.GHE.LOCALHOST')
const isGheHost = hostname.endsWith('.GHE.COM')
const isLocalHost = hostname.endsWith('.LOCALHOST')
return !isGitHubHost && !isGheHost
return !isGitHubHost && !isGheHost && !isLocalHost
}
export function getGitHubWorkspaceDir(): string {
+3
View File
@@ -12,6 +12,9 @@ Once the attestation has been created and signed, it will be uploaded to the GH
attestations API and associated with the repository from which the workflow was
initiated.
See [Using artifact attestations to establish provenance for builds](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)
for more information on artifact attestations.
## Usage
### `attest`
+13
View File
@@ -1,10 +1,23 @@
# @actions/attest Releases
### 1.3.0
- Dynamic construction of Sigstore API URLs
- Switch to new GH provenance build type
- Fetch existing Rekor entry on 409 conflict error
- Bump @sigstore/bundle from 2.3.0 to 2.3.2
- Bump @sigstore/sign from 2.3.0 to 2.3.2
### 1.2.1
- Retry request on attestation persistence failure
### 1.2.0
- Generate attestations using the v0.3 Sigstore bundle format.
- Bump @sigstore/bundle from 2.2.0 to 2.3.0.
- Bump @sigstore/sign from 2.2.3 to 2.3.0.
- Remove dependency on make-fetch-happen
### 1.1.0
@@ -4,7 +4,7 @@ exports[`provenance functions buildSLSAProvenancePredicate returns a provenance
{
"params": {
"buildDefinition": {
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
"buildType": "https://actions.github.io/buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"path": ".github/workflows/main.yml",
@@ -17,6 +17,7 @@ exports[`provenance functions buildSLSAProvenancePredicate returns a provenance
"event_name": "push",
"repository_id": "repo-id",
"repository_owner_id": "owner-id",
"runner_environment": "github-hosted",
},
},
"resolvedDependencies": [
@@ -30,7 +31,7 @@ exports[`provenance functions buildSLSAProvenancePredicate returns a provenance
},
"runDetails": {
"builder": {
"id": "https://github.com/actions/runner/github-hosted",
"id": "https://github.com/owner/workflows/.github/workflows/publish.yml@main",
},
"metadata": {
"invocationId": "https://github.com/owner/repo/actions/runs/run-id/attempts/run-attempt",
@@ -0,0 +1,41 @@
import {signingEndpoints} from '../src/endpoints'
describe('signingEndpoints', () => {
const originalEnv = process.env
afterEach(() => {
process.env = originalEnv
})
describe('when using github.com', () => {
beforeEach(async () => {
process.env = {
...originalEnv,
GITHUB_SERVER_URL: 'https://github.com'
}
})
it('returns expected endpoints', async () => {
const endpoints = signingEndpoints('github')
expect(endpoints.fulcioURL).toEqual('https://fulcio.githubapp.com')
expect(endpoints.tsaServerURL).toEqual('https://timestamp.githubapp.com')
})
})
describe('when using custom domain', () => {
beforeEach(async () => {
process.env = {
...originalEnv,
GITHUB_SERVER_URL: 'https://foo.bar.com'
}
})
it('returns a expected endpoints', async () => {
const endpoints = signingEndpoints('github')
expect(endpoints.fulcioURL).toEqual('https://fulcio.foo.bar.com')
expect(endpoints.tsaServerURL).toEqual('https://timestamp.foo.bar.com')
})
})
})
+2 -1
View File
@@ -45,7 +45,8 @@ describe('getIDTokenClaims', () => {
sha: 'sha',
repository: 'repo',
event_name: 'push',
workflow_ref: 'main',
job_workflow_ref: 'job_workflow_ref',
workflow_ref: 'workflow',
repository_id: '1',
repository_owner_id: '1',
runner_environment: 'github-hosted',
+20 -7
View File
@@ -2,7 +2,8 @@ import * as github from '@actions/github'
import {mockFulcio, mockRekor, mockTSA} from '@sigstore/mock'
import * as jose from 'jose'
import nock from 'nock'
import {SIGSTORE_GITHUB, SIGSTORE_PUBLIC_GOOD} from '../src/endpoints'
import {MockAgent, setGlobalDispatcher} from 'undici'
import {SIGSTORE_PUBLIC_GOOD, signingEndpoints} from '../src/endpoints'
import {attestProvenance, buildSLSAProvenancePredicate} from '../src/provenance'
describe('provenance functions', () => {
@@ -12,12 +13,17 @@ describe('provenance functions', () => {
const jwksPath = '/.well-known/jwks.json'
const tokenPath = '/token'
// MockAgent for mocking @actions/github
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const claims = {
iss: issuer,
aud: 'nobody',
repository: 'owner/repo',
ref: 'refs/heads/main',
sha: 'babca52ab0c93ae16539e5923cb0d7403b9a093b',
job_workflow_ref: 'owner/workflows/.github/workflows/publish.yml@main',
workflow_ref: 'owner/repo/.github/workflows/main.yml@main',
event_name: 'push',
repository_id: 'repo-id',
@@ -90,16 +96,19 @@ describe('provenance functions', () => {
})
describe('when using the github Sigstore instance', () => {
const {fulcioURL, tsaServerURL} = SIGSTORE_GITHUB
const {fulcioURL, tsaServerURL} = signingEndpoints('github')
beforeEach(async () => {
// Mock Sigstore
await mockFulcio({baseURL: fulcioURL, strict: false})
await mockTSA({baseURL: tsaServerURL})
// Mock GH attestations API
nock('https://api.github.com')
.post(/^\/repos\/.*\/.*\/attestations$/)
mockAgent
.get('https://api.github.com')
.intercept({
path: /^\/repos\/.*\/.*\/attestations$/,
method: 'post'
})
.reply(201, {id: attestationID})
})
@@ -159,8 +168,12 @@ describe('provenance functions', () => {
await mockRekor({baseURL: rekorURL})
// Mock GH attestations API
nock('https://api.github.com')
.post(/^\/repos\/.*\/.*\/attestations$/)
mockAgent
.get('https://api.github.com')
.intercept({
path: /^\/repos\/.*\/.*\/attestations$/,
method: 'post'
})
.reply(201, {id: attestationID})
})
+53 -8
View File
@@ -1,4 +1,4 @@
import nock from 'nock'
import {MockAgent, setGlobalDispatcher} from 'undici'
import {writeAttestation} from '../src/store'
describe('writeAttestation', () => {
@@ -6,6 +6,9 @@ describe('writeAttestation', () => {
const attestation = {foo: 'bar '}
const token = 'token'
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
beforeEach(() => {
process.env = {
...originalEnv,
@@ -19,9 +22,14 @@ describe('writeAttestation', () => {
describe('when the api call is successful', () => {
beforeEach(() => {
nock('https://api.github.com')
.matchHeader('authorization', `token ${token}`)
.post('/repos/foo/bar/attestations', {bundle: attestation})
mockAgent
.get('https://api.github.com')
.intercept({
path: '/repos/foo/bar/attestations',
method: 'POST',
headers: {authorization: `token ${token}`},
body: JSON.stringify({bundle: attestation})
})
.reply(201, {id: '123'})
})
@@ -32,14 +40,51 @@ describe('writeAttestation', () => {
describe('when the api call fails', () => {
beforeEach(() => {
nock('https://api.github.com')
.matchHeader('authorization', `token ${token}`)
.post('/repos/foo/bar/attestations', {bundle: attestation})
mockAgent
.get('https://api.github.com')
.intercept({
path: '/repos/foo/bar/attestations',
method: 'POST',
headers: {authorization: `token ${token}`},
body: JSON.stringify({bundle: attestation})
})
.reply(500, 'oops')
})
it('throws an error', async () => {
await expect(writeAttestation(attestation, token)).rejects.toThrow(/oops/)
await expect(
writeAttestation(attestation, token, {retry: 0})
).rejects.toThrow(/oops/)
})
})
describe('when the api call fails but succeeds on retry', () => {
beforeEach(() => {
const pool = mockAgent.get('https://api.github.com')
pool
.intercept({
path: '/repos/foo/bar/attestations',
method: 'POST',
headers: {authorization: `token ${token}`},
body: JSON.stringify({bundle: attestation})
})
.reply(500, 'oops')
.times(1)
pool
.intercept({
path: '/repos/foo/bar/attestations',
method: 'POST',
headers: {authorization: `token ${token}`},
body: JSON.stringify({bundle: attestation})
})
.reply(201, {id: '123'})
.times(1)
})
it('persists the attestation', async () => {
await expect(writeAttestation(attestation, token)).resolves.toEqual('123')
})
})
})
+446 -432
View File
File diff suppressed because it is too large Load Diff
+13 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/attest",
"version": "1.2.0",
"version": "1.3.0",
"description": "Actions attestation lib",
"keywords": [
"github",
@@ -35,21 +35,26 @@
"url": "https://github.com/actions/toolkit/issues"
},
"devDependencies": {
"@sigstore/mock": "^0.6.5",
"@sigstore/mock": "^0.7.4",
"@sigstore/rekor-types": "^2.0.0",
"@types/jsonwebtoken": "^9.0.6",
"@types/make-fetch-happen": "^10.0.4",
"jose": "^5.2.3",
"nock": "^13.5.1"
"nock": "^13.5.1",
"undici": "^5.28.4"
},
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0",
"@actions/http-client": "^2.2.1",
"@sigstore/bundle": "^2.3.0",
"@sigstore/sign": "^2.3.0",
"@octokit/plugin-retry": "^6.0.1",
"@sigstore/bundle": "^2.3.2",
"@sigstore/sign": "^2.3.2",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0",
"make-fetch-happen": "^13.0.0"
"jwks-rsa": "^3.1.0"
},
"overrides": {
"@octokit/plugin-retry": {
"@octokit/core": "^5.2.0"
}
}
}
+14 -9
View File
@@ -6,9 +6,6 @@ const GITHUB_ID = 'github'
const FULCIO_PUBLIC_GOOD_URL = 'https://fulcio.sigstore.dev'
const REKOR_PUBLIC_GOOD_URL = 'https://rekor.sigstore.dev'
const FULCIO_INTERNAL_URL = 'https://fulcio.githubapp.com'
const TSA_INTERNAL_URL = 'https://timestamp.githubapp.com'
export type SigstoreInstance = typeof PUBLIC_GOOD_ID | typeof GITHUB_ID
export type Endpoints = {
@@ -22,11 +19,6 @@ export const SIGSTORE_PUBLIC_GOOD: Endpoints = {
rekorURL: REKOR_PUBLIC_GOOD_URL
}
export const SIGSTORE_GITHUB: Endpoints = {
fulcioURL: FULCIO_INTERNAL_URL,
tsaServerURL: TSA_INTERNAL_URL
}
export const signingEndpoints = (sigstore?: SigstoreInstance): Endpoints => {
let instance: SigstoreInstance
@@ -45,6 +37,19 @@ export const signingEndpoints = (sigstore?: SigstoreInstance): Endpoints => {
case PUBLIC_GOOD_ID:
return SIGSTORE_PUBLIC_GOOD
case GITHUB_ID:
return SIGSTORE_GITHUB
return buildGitHubEndpoints()
}
}
function buildGitHubEndpoints(): Endpoints {
const serverURL = process.env.GITHUB_SERVER_URL || 'https://github.com'
let host = new URL(serverURL).hostname
if (host === 'github.com') {
host = 'githubapp.com'
}
return {
fulcioURL: `https://fulcio.${host}`,
tsaServerURL: `https://timestamp.${host}`
}
}
+1
View File
@@ -11,6 +11,7 @@ const REQUIRED_CLAIMS = [
'sha',
'repository',
'event_name',
'job_workflow_ref',
'workflow_ref',
'repository_id',
'repository_owner_id',
+4 -6
View File
@@ -3,10 +3,7 @@ import {getIDTokenClaims} from './oidc'
import type {Attestation, Predicate} from './shared.types'
const SLSA_PREDICATE_V1_TYPE = 'https://slsa.dev/provenance/v1'
const GITHUB_BUILDER_ID_PREFIX = 'https://github.com/actions/runner'
const GITHUB_BUILD_TYPE =
'https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1'
const GITHUB_BUILD_TYPE = 'https://actions.github.io/buildtypes/workflow/v1'
const DEFAULT_ISSUER = 'https://token.actions.githubusercontent.com'
@@ -55,7 +52,8 @@ export const buildSLSAProvenancePredicate = async (
github: {
event_name: claims.event_name,
repository_id: claims.repository_id,
repository_owner_id: claims.repository_owner_id
repository_owner_id: claims.repository_owner_id,
runner_environment: claims.runner_environment
}
},
resolvedDependencies: [
@@ -69,7 +67,7 @@ export const buildSLSAProvenancePredicate = async (
},
runDetails: {
builder: {
id: `${GITHUB_BUILDER_ID_PREFIX}/${claims.runner_environment}`
id: `${serverURL}/${claims.job_workflow_ref}`
},
metadata: {
invocationId: `${serverURL}/${claims.repository}/actions/runs/${claims.run_id}/attempts/${claims.run_attempt}`
+1
View File
@@ -87,6 +87,7 @@ const initBundleBuilder = (opts: SignOptions): BundleBuilder => {
new RekorWitness({
rekorBaseURL: opts.rekorURL,
entryType: 'dsse',
fetchOnConflict: true,
timeout,
retry
})
+14 -4
View File
@@ -1,8 +1,12 @@
import * as github from '@actions/github'
import fetch from 'make-fetch-happen'
import {retry} from '@octokit/plugin-retry'
const CREATE_ATTESTATION_REQUEST = 'POST /repos/{owner}/{repo}/attestations'
const DEFAULT_RETRY_COUNT = 5
export type WriteOptions = {
retry?: number
}
/**
* Writes an attestation to the repository's attestations endpoint.
* @param attestation - The attestation to write.
@@ -12,9 +16,11 @@ const CREATE_ATTESTATION_REQUEST = 'POST /repos/{owner}/{repo}/attestations'
*/
export const writeAttestation = async (
attestation: unknown,
token: string
token: string,
options: WriteOptions = {}
): Promise<string> => {
const octokit = github.getOctokit(token, {request: {fetch}})
const retries = options.retry ?? DEFAULT_RETRY_COUNT
const octokit = github.getOctokit(token, {retry: {retries}}, retry)
try {
const response = await octokit.request(CREATE_ATTESTATION_REQUEST, {
@@ -23,7 +29,11 @@ export const writeAttestation = async (
data: {bundle: attestation}
})
return response.data?.id
const data =
typeof response.data == 'string'
? JSON.parse(response.data)
: response.data
return data?.id
} catch (err) {
const message = err instanceof Error ? err.message : err
throw new Error(`Failed to persist attestation: ${message}`)