upload attestation to GHCR instead of attestations API

This commit is contained in:
Conor Sloan
2024-08-22 14:08:50 +01:00
parent f213f0c945
commit 1f725c56d6
7 changed files with 1016 additions and 106 deletions
+143 -50
View File
@@ -13,6 +13,7 @@ import * as cfg from '../src/config'
import * as fsHelper from '../src/fs-helper' import * as fsHelper from '../src/fs-helper'
import * as ghcr from '../src/ghcr-client' import * as ghcr from '../src/ghcr-client'
import * as ociContainer from '../src/oci-container' import * as ociContainer from '../src/oci-container'
import * as oci from '@sigstore/oci'
const ghcrUrl = new URL('https://ghcr.io') const ghcrUrl = new URL('https://ghcr.io')
@@ -38,6 +39,9 @@ let resolvePublishActionOptionsMock: jest.SpyInstance
// Mock generating attestation // Mock generating attestation
let generateAttestationMock: jest.SpyInstance let generateAttestationMock: jest.SpyInstance
// Mock uploading attestation with oci lib
let attachArtifactToImageMock: jest.SpyInstance
describe('run', () => { describe('run', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
@@ -79,6 +83,9 @@ describe('run', () => {
generateAttestationMock = jest generateAttestationMock = jest
.spyOn(attest, 'attestProvenance') .spyOn(attest, 'attestProvenance')
.mockImplementation() .mockImplementation()
attachArtifactToImageMock = jest
.spyOn(oci, 'attachArtifactToImage')
.mockImplementation()
}) })
it('fails if the action ref is not a tag', async () => { it('fails if the action ref is not a tag', async () => {
@@ -200,47 +207,6 @@ describe('run', () => {
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong') expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
}) })
it('fails if creating attestation fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
calculateManifestDigestMock.mockImplementation(() => {
return 'sha256:my-test-digest'
})
generateAttestationMock.mockImplementation(async () => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if publishing OCI artifact fails', async () => { it('fails if publishing OCI artifact fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions()) resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
@@ -272,7 +238,7 @@ describe('run', () => {
}) })
generateAttestationMock.mockImplementation(async options => { generateAttestationMock.mockImplementation(async options => {
expect(options).toHaveProperty('skipWrite', false) expect(options).toHaveProperty('skipWrite', true)
return { return {
attestationID: 'test-attestation-id', attestationID: 'test-attestation-id',
@@ -330,7 +296,7 @@ describe('run', () => {
}) })
generateAttestationMock.mockImplementation(async options => { generateAttestationMock.mockImplementation(async options => {
expect(options).toHaveProperty('skipWrite', false) expect(options).toHaveProperty('skipWrite', true)
return { return {
attestationID: 'test-attestation-id', attestationID: 'test-attestation-id',
@@ -362,6 +328,119 @@ describe('run', () => {
) )
}) })
it('fails if uploading attestation to GHCR fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
calculateManifestDigestMock.mockImplementation(() => {
return 'sha256:my-test-digest'
})
publishOCIArtifactMock.mockImplementation(() => {
return {
packageURL: 'https://ghcr.io/v2/test-org/test-repo:1.2.3',
publishedDigest: 'sha256:my-test-digest'
}
})
generateAttestationMock.mockImplementation(async options => {
expect(options).toHaveProperty('skipWrite', true)
return {
attestationID: 'test-attestation-id',
certificate: 'test',
bundle: {
mediaType: 'application/vnd.cncf.notary.v2+jwt',
verificationMaterial: {
publicKey: {
hint: 'test-hint'
}
}
}
}
})
attachArtifactToImageMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if creating attestation fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
calculateManifestDigestMock.mockImplementation(() => {
return 'sha256:my-test-digest'
})
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
publishOCIArtifactMock.mockImplementation(() => {
return {
packageURL: 'https://ghcr.io/v2/test-org/test-repo:1.2.3',
publishedDigest: 'sha256:my-test-digest'
}
})
generateAttestationMock.mockImplementation(async () => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('uploads the artifact, returns package metadata from GHCR, and skips writing attestation in enterprise', async () => { it('uploads the artifact, returns package metadata from GHCR, and skips writing attestation in enterprise', async () => {
const options = baseOptions() const options = baseOptions()
options.isEnterprise = true options.isEnterprise = true
@@ -464,7 +543,7 @@ describe('run', () => {
}) })
generateAttestationMock.mockImplementation(async options => { generateAttestationMock.mockImplementation(async options => {
expect(options).toHaveProperty('skipWrite', false) expect(options).toHaveProperty('skipWrite', true)
return { return {
attestationID: 'test-attestation-id', attestationID: 'test-attestation-id',
@@ -480,6 +559,15 @@ describe('run', () => {
} }
}) })
attachArtifactToImageMock.mockImplementation(async () => {
return {
digest: 'sha256:my-test-attestation-digest',
urls: [
'ghcr.io/v2/test-org/test-package/manifests/sha256:my-test-attestation-digest'
]
}
})
// Run the action // Run the action
await main.run() await main.run()
@@ -487,7 +575,17 @@ describe('run', () => {
expect(publishOCIArtifactMock).toHaveBeenCalledTimes(1) expect(publishOCIArtifactMock).toHaveBeenCalledTimes(1)
// Check outputs // Check outputs
expect(setOutputMock).toHaveBeenCalledTimes(4) expect(setOutputMock).toHaveBeenCalledTimes(5)
expect(setOutputMock).toHaveBeenCalledWith(
'attestation-manifest-sha',
'sha256:my-test-attestation-digest'
)
expect(setOutputMock).toHaveBeenCalledWith(
'attestation-url',
'ghcr.io/v2/test-org/test-package/manifests/sha256:my-test-attestation-digest'
)
expect(setOutputMock).toHaveBeenCalledWith( expect(setOutputMock).toHaveBeenCalledWith(
'package-url', 'package-url',
@@ -503,11 +601,6 @@ describe('run', () => {
'package-manifest-sha', 'package-manifest-sha',
'sha256:my-test-digest' 'sha256:my-test-digest'
) )
expect(setOutputMock).toHaveBeenCalledWith(
'attestation-id',
'test-attestation-id'
)
}) })
}) })
+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 97.06%"><title>Coverage: 97.06%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">97.06%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">97.06%</text></g></svg> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 97.17%"><title>Coverage: 97.17%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">97.17%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">97.17%</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Generated Vendored
+805 -34
View File
@@ -52,7 +52,7 @@ function attest(options) {
// Store the attestation // Store the attestation
let attestationID; let attestationID;
if (options.skipWrite !== true) { if (options.skipWrite !== true) {
attestationID = yield (0, store_1.writeAttestation)((0, bundle_1.bundleToJSON)(bundle), options.token, { headers: options.headers }); attestationID = yield (0, store_1.writeAttestation)((0, bundle_1.bundleToJSON)(bundle), options.token);
} }
return toAttestation(bundle, attestationID); return toAttestation(bundle, attestationID);
}); });
@@ -249,10 +249,6 @@ const core_1 = __nccwpck_require__(42186);
const http_client_1 = __nccwpck_require__(96255); const http_client_1 = __nccwpck_require__(96255);
const jose = __importStar(__nccwpck_require__(34061)); const jose = __importStar(__nccwpck_require__(34061));
const OIDC_AUDIENCE = 'nobody'; const OIDC_AUDIENCE = 'nobody';
const VALID_SERVER_URLS = [
'https://github.com',
new RegExp('^https://[a-z0-9-]+\\.ghe\\.com$')
];
const REQUIRED_CLAIMS = [ const REQUIRED_CLAIMS = [
'iss', 'iss',
'ref', 'ref',
@@ -268,7 +264,6 @@ const REQUIRED_CLAIMS = [
'run_attempt' 'run_attempt'
]; ];
const getIDTokenClaims = (issuer) => __awaiter(void 0, void 0, void 0, function* () { const getIDTokenClaims = (issuer) => __awaiter(void 0, void 0, void 0, function* () {
issuer = issuer || getIssuer();
try { try {
const token = yield (0, core_1.getIDToken)(OIDC_AUDIENCE); const token = yield (0, core_1.getIDToken)(OIDC_AUDIENCE);
const claims = yield decodeOIDCToken(token, issuer); const claims = yield decodeOIDCToken(token, issuer);
@@ -312,19 +307,6 @@ function assertClaimSet(claims) {
throw new Error(`Missing claims: ${missingClaims.join(', ')}`); throw new Error(`Missing claims: ${missingClaims.join(', ')}`);
} }
} }
// Derive the current OIDC issuer based on the server URL
function getIssuer() {
const serverURL = process.env.GITHUB_SERVER_URL || 'https://github.com';
// Ensure the server URL is a valid GitHub server URL
if (!VALID_SERVER_URLS.some(valid_url => serverURL.match(valid_url))) {
throw new Error(`Invalid server URL: ${serverURL}`);
}
let host = new URL(serverURL).hostname;
if (host === 'github.com') {
host = 'githubusercontent.com';
}
return `https://token.actions.${host}`;
}
//# sourceMappingURL=oidc.js.map //# sourceMappingURL=oidc.js.map
/***/ }), /***/ }),
@@ -349,6 +331,7 @@ const attest_1 = __nccwpck_require__(46373);
const oidc_1 = __nccwpck_require__(95847); const oidc_1 = __nccwpck_require__(95847);
const SLSA_PREDICATE_V1_TYPE = 'https://slsa.dev/provenance/v1'; const SLSA_PREDICATE_V1_TYPE = 'https://slsa.dev/provenance/v1';
const GITHUB_BUILD_TYPE = 'https://actions.github.io/buildtypes/workflow/v1'; const GITHUB_BUILD_TYPE = 'https://actions.github.io/buildtypes/workflow/v1';
const DEFAULT_ISSUER = 'https://token.actions.githubusercontent.com';
/** /**
* Builds an SLSA (Supply Chain Levels for Software Artifacts) provenance * Builds an SLSA (Supply Chain Levels for Software Artifacts) provenance
* predicate using the GitHub Actions Workflow build type. * predicate using the GitHub Actions Workflow build type.
@@ -358,7 +341,7 @@ const GITHUB_BUILD_TYPE = 'https://actions.github.io/buildtypes/workflow/v1';
* issuer. * issuer.
* @returns The SLSA provenance predicate. * @returns The SLSA provenance predicate.
*/ */
const buildSLSAProvenancePredicate = (issuer) => __awaiter(void 0, void 0, void 0, function* () { const buildSLSAProvenancePredicate = (issuer = DEFAULT_ISSUER) => __awaiter(void 0, void 0, void 0, function* () {
const serverURL = process.env.GITHUB_SERVER_URL; const serverURL = process.env.GITHUB_SERVER_URL;
const claims = yield (0, oidc_1.getIDTokenClaims)(issuer); const claims = yield (0, oidc_1.getIDTokenClaims)(issuer);
// Split just the path and ref from the workflow string. // Split just the path and ref from the workflow string.
@@ -557,7 +540,6 @@ const writeAttestation = (attestation, token, options = {}) => __awaiter(void 0,
const response = yield octokit.request(CREATE_ATTESTATION_REQUEST, { const response = yield octokit.request(CREATE_ATTESTATION_REQUEST, {
owner: github.context.repo.owner, owner: github.context.repo.owner,
repo: github.context.repo.repo, repo: github.context.repo.repo,
headers: options.headers,
data: { bundle: attestation } data: { bundle: attestation }
}); });
const data = typeof response.data == 'string' const data = typeof response.data == 'string'
@@ -11756,6 +11738,761 @@ class SignedCertificateTimestamp {
exports.SignedCertificateTimestamp = SignedCertificateTimestamp; exports.SignedCertificateTimestamp = SignedCertificateTimestamp;
/***/ }),
/***/ 61319:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.HEADER_OCI_SUBJECT = exports.HEADER_LOCATION = exports.HEADER_IF_MATCH = exports.HEADER_ETAG = exports.HEADER_DIGEST = exports.HEADER_CONTENT_TYPE = exports.HEADER_CONTENT_LENGTH = exports.HEADER_AUTHORIZATION = exports.HEADER_AUTHENTICATE = exports.HEADER_API_VERSION = exports.HEADER_ACCEPT = exports.CONTENT_TYPE_EMPTY_DESCRIPTOR = exports.CONTENT_TYPE_OCTET_STREAM = exports.CONTENT_TYPE_DOCKER_MANIFEST_LIST = exports.CONTENT_TYPE_DOCKER_MANIFEST = exports.CONTENT_TYPE_OCI_MANIFEST = exports.CONTENT_TYPE_OCI_INDEX = void 0;
/*
Copyright 2023 The Sigstore Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
exports.CONTENT_TYPE_OCI_INDEX = 'application/vnd.oci.image.index.v1+json';
exports.CONTENT_TYPE_OCI_MANIFEST = 'application/vnd.oci.image.manifest.v1+json';
exports.CONTENT_TYPE_DOCKER_MANIFEST = 'application/vnd.docker.distribution.manifest.v2+json';
exports.CONTENT_TYPE_DOCKER_MANIFEST_LIST = 'application/vnd.docker.distribution.manifest.list.v2+json';
exports.CONTENT_TYPE_OCTET_STREAM = 'application/octet-stream';
exports.CONTENT_TYPE_EMPTY_DESCRIPTOR = 'application/vnd.oci.empty.v1+json';
exports.HEADER_ACCEPT = 'Accept';
exports.HEADER_API_VERSION = 'Docker-Distribution-API-Version';
exports.HEADER_AUTHENTICATE = 'WWW-Authenticate';
exports.HEADER_AUTHORIZATION = 'Authorization';
exports.HEADER_CONTENT_LENGTH = 'Content-Length';
exports.HEADER_CONTENT_TYPE = 'Content-Type';
exports.HEADER_DIGEST = 'Docker-Content-Digest';
exports.HEADER_ETAG = 'Etag';
exports.HEADER_IF_MATCH = 'If-Match';
exports.HEADER_LOCATION = 'Location';
exports.HEADER_OCI_SUBJECT = 'OCI-Subject';
/***/ }),
/***/ 95475:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.fromBasicAuth = exports.toBasicAuth = exports.getRegistryCredentials = void 0;
/*
Copyright 2023 The Sigstore Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const node_fs_1 = __importDefault(__nccwpck_require__(87561));
const node_os_1 = __importDefault(__nccwpck_require__(70612));
const node_path_1 = __importDefault(__nccwpck_require__(49411));
const name_1 = __nccwpck_require__(44520);
// Returns the credentials for a given registry by reading the Docker config
// file.
const getRegistryCredentials = (imageName) => {
const { registry } = (0, name_1.parseImageName)(imageName);
const dockerConfigFile = node_path_1.default.join(node_os_1.default.homedir(), '.docker', 'config.json');
let content;
try {
content = node_fs_1.default.readFileSync(dockerConfigFile, 'utf8');
}
catch (err) {
throw new Error(`No credential file found at ${dockerConfigFile}`);
}
const dockerConfig = JSON.parse(content);
const credKey = Object.keys(dockerConfig?.auths || {}).find((key) => key.includes(registry)) || registry;
const creds = dockerConfig?.auths?.[credKey];
if (!creds) {
throw new Error(`No credentials found for registry ${registry}`);
}
// Extract username/password from auth string
const { username, password } = (0, exports.fromBasicAuth)(creds.auth);
// If the identitytoken is present, use it as the password (primarily for ACR)
const pass = creds.identitytoken ? creds.identitytoken : password;
return { username, password: pass };
};
exports.getRegistryCredentials = getRegistryCredentials;
// Encode the username and password as base64-encoded basicauth value
const toBasicAuth = (creds) => Buffer.from(`${creds.username}:${creds.password}`).toString('base64');
exports.toBasicAuth = toBasicAuth;
// Decode the base64-encoded basicauth value
const fromBasicAuth = (auth) => {
// Need to account for the possibility of ':' in the password
const [username, ...rest] = Buffer.from(auth, 'base64').toString().split(':');
const password = rest.join(':');
return { username, password };
};
exports.fromBasicAuth = fromBasicAuth;
/***/ }),
/***/ 60064:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.OCIError = exports.ensureStatus = exports.HTTPError = void 0;
class HTTPError extends Error {
constructor({ status, message }) {
super(message);
this.statusCode = status;
}
}
exports.HTTPError = HTTPError;
// Inspects the response status and throws an HTTPError if it does not match the
// expected status code
const ensureStatus = (expectedStatus) => {
return (response) => {
if (response.status !== expectedStatus) {
throw new HTTPError({
message: `Error fetching ${response.url} - expected ${expectedStatus}, received ${response.status}`,
status: response.status,
});
}
return response;
};
};
exports.ensureStatus = ensureStatus;
class OCIError extends Error {
constructor({ message, cause, }) {
super(message);
this.cause = cause;
this.name = this.constructor.name;
}
}
exports.OCIError = OCIError;
/***/ }),
/***/ 437:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
/*
Copyright 2024 The Sigstore Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const http2_1 = __nccwpck_require__(85158);
const make_fetch_happen_1 = __importDefault(__nccwpck_require__(9525));
const proc_log_1 = __nccwpck_require__(56528);
const promise_retry_1 = __importDefault(__nccwpck_require__(54742));
const { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_TOO_MANY_REQUESTS, HTTP_STATUS_REQUEST_TIMEOUT, } = http2_1.constants;
const fetchWithRetry = async (url, options = {}) => {
return (0, promise_retry_1.default)(async (retry, attemptNum) => {
/* eslint-disable @typescript-eslint/no-explicit-any */
const logRetry = (reason) => {
proc_log_1.log.http('fetch', `${options.method} ${url} attempt ${attemptNum} failed with ${reason}`);
};
const response = await (0, make_fetch_happen_1.default)(url, {
...options,
retry: false, // We're handling retries ourselves
}).catch((reason) => {
logRetry(reason);
return retry(reason);
});
if (retryable(response.status)) {
logRetry(response.status);
return retry(response);
}
return response;
}, retryOpts(options.retry)).catch((err) => {
// If we got an actual error, throw it
if (err instanceof Error) {
throw err;
}
// Otherwise, return the response (this is simply a retry-able response for
// which we exceeded the retry limit)
return err;
});
};
// Returns a wrapped fetch function with default options
fetchWithRetry.defaults = (defaultOptions = {}, wrappedFetch = fetchWithRetry) => {
const defaultedFetch = (url, options = {}) => {
const finalOptions = {
...defaultOptions,
...options,
headers: { ...defaultOptions.headers, ...options.headers },
};
return wrappedFetch(url, finalOptions);
};
defaultedFetch.defaults = (newDefaults = {}) => fetchWithRetry.defaults(newDefaults, defaultedFetch);
return defaultedFetch;
};
// Determine if a status code is retryable. This includes 5xx errors, 408, and
// 429.
const retryable = (status) => [HTTP_STATUS_REQUEST_TIMEOUT, HTTP_STATUS_TOO_MANY_REQUESTS].includes(status) || status >= HTTP_STATUS_INTERNAL_SERVER_ERROR;
// Normalize the retry options to the format expected by promise-retry
const retryOpts = (retry) => {
if (typeof retry === 'boolean') {
return { retries: retry ? 1 : 0 };
}
else if (typeof retry === 'number') {
return { retries: retry };
}
else {
return { retries: 0, ...retry };
}
};
exports["default"] = fetchWithRetry;
/***/ }),
/***/ 79539:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _OCIImage_instances, _OCIImage_client, _OCIImage_credentials, _OCIImage_createReferrersIndexByTag;
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.OCIImage = void 0;
/*
Copyright 2023 The Sigstore Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const constants_1 = __nccwpck_require__(61319);
const error_1 = __nccwpck_require__(60064);
const registry_1 = __nccwpck_require__(27464);
const DOCKER_DEFAULT_REGISTRY = 'registry-1.docker.io';
const EMPTY_BLOB = Buffer.from('{}');
class OCIImage {
constructor(image, creds, opts) {
_OCIImage_instances.add(this);
_OCIImage_client.set(this, void 0);
_OCIImage_credentials.set(this, void 0);
__classPrivateFieldSet(this, _OCIImage_client, new registry_1.RegistryClient(canonicalizeRegistryName(image.registry), image.path, opts), "f");
__classPrivateFieldSet(this, _OCIImage_credentials, creds, "f");
}
async addArtifact(opts) {
let artifactDescriptor;
const annotations = {
'org.opencontainers.image.created': new Date().toISOString(),
...opts.annotations,
};
try {
if (__classPrivateFieldGet(this, _OCIImage_credentials, "f")) {
await __classPrivateFieldGet(this, _OCIImage_client, "f").signIn(__classPrivateFieldGet(this, _OCIImage_credentials, "f"));
}
// Check that the image exists
const imageDescriptor = await __classPrivateFieldGet(this, _OCIImage_client, "f").checkManifest(opts.imageDigest);
// Upload the artifact blob
const artifactBlob = await __classPrivateFieldGet(this, _OCIImage_client, "f").uploadBlob(opts.artifact);
// Upload the empty blob (needed for the manifest config)
const emptyBlob = await __classPrivateFieldGet(this, _OCIImage_client, "f").uploadBlob(EMPTY_BLOB);
// Construct artifact manifest
const manifest = buildManifest({
artifactDescriptor: { ...artifactBlob, mediaType: opts.mediaType },
subjectDescriptor: imageDescriptor,
configDescriptor: {
...emptyBlob,
mediaType: constants_1.CONTENT_TYPE_EMPTY_DESCRIPTOR,
},
annotations,
});
// Upload artifact manifest
artifactDescriptor = await __classPrivateFieldGet(this, _OCIImage_client, "f").uploadManifest(JSON.stringify(manifest));
// Check to see if registry supports the referrers API. For most
// registries the presence of a subjectDigest response header when
// uploading the artifact manifest indicates that the referrers API IS
// supported -- however, this is not a guarantee (AWS ECR does NOT support
// the referrers API but still reports a subjectDigest).
const referrersSupported = await __classPrivateFieldGet(this, _OCIImage_client, "f").pingReferrers();
// Manually update the referrers list if the referrers API is not supported.
if (!artifactDescriptor.subjectDigest || !referrersSupported) {
// Strip subjectDigest from the artifact descriptor (in case it was returned)
/* eslint-disable-next-line @typescript-eslint/no-unused-vars */
const { subjectDigest, ...descriptor } = artifactDescriptor;
await __classPrivateFieldGet(this, _OCIImage_instances, "m", _OCIImage_createReferrersIndexByTag).call(this, {
artifact: {
...descriptor,
artifactType: opts.mediaType,
annotations,
},
imageDigest: opts.imageDigest,
});
}
}
catch (err) {
throw new error_1.OCIError({
message: `Error uploading artifact to container registry`,
cause: err,
});
}
return artifactDescriptor;
}
async getDigest(tag) {
try {
if (__classPrivateFieldGet(this, _OCIImage_credentials, "f")) {
await __classPrivateFieldGet(this, _OCIImage_client, "f").signIn(__classPrivateFieldGet(this, _OCIImage_credentials, "f"));
}
const imageDescriptor = await __classPrivateFieldGet(this, _OCIImage_client, "f").checkManifest(tag);
return imageDescriptor.digest;
}
catch (err) {
throw new error_1.OCIError({
message: `Error retrieving image digest from container registry`,
cause: err,
});
}
}
}
exports.OCIImage = OCIImage;
_OCIImage_client = new WeakMap(), _OCIImage_credentials = new WeakMap(), _OCIImage_instances = new WeakSet(), _OCIImage_createReferrersIndexByTag =
// Create a referrers index by tag. This is a fallback for registries that do
// not support the referrers API.
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests-with-subject
async function _OCIImage_createReferrersIndexByTag(opts) {
const referrerTag = digestToTag(opts.imageDigest);
let referrerManifest;
let etag;
try {
// Retrieve any existing referrer index
const referrerIndex = await __classPrivateFieldGet(this, _OCIImage_client, "f").getManifest(referrerTag);
if (referrerIndex.mediaType !== constants_1.CONTENT_TYPE_OCI_INDEX) {
throw new Error(`Expected referrer manifest type ${constants_1.CONTENT_TYPE_OCI_INDEX}, got ${referrerIndex.mediaType}`);
}
referrerManifest = referrerIndex.body;
etag = referrerIndex.etag;
}
catch (err) {
// If the referrer index does not exist, create a new one
if (err instanceof error_1.HTTPError && err.statusCode === 404) {
referrerManifest = newIndex();
}
else {
throw err;
}
}
// If the artifact is not already in the index, add it to the list and
// re-upload the index
if (!referrerManifest.manifests.some((manifest) => manifest.digest === opts.artifact.digest)) {
// Add the artifact to the index
referrerManifest.manifests.push(opts.artifact);
await __classPrivateFieldGet(this, _OCIImage_client, "f").uploadManifest(JSON.stringify(referrerManifest), {
mediaType: constants_1.CONTENT_TYPE_OCI_INDEX,
reference: referrerTag,
etag,
});
}
};
// Build an OCI manifest document with references to the given artifact,
// subject, and config
const buildManifest = (opts) => ({
schemaVersion: 2,
mediaType: constants_1.CONTENT_TYPE_OCI_MANIFEST,
artifactType: opts.artifactDescriptor.mediaType,
config: opts.configDescriptor,
layers: [opts.artifactDescriptor],
subject: opts.subjectDescriptor,
annotations: opts.annotations,
});
// Return an empty OCI index document
const newIndex = () => ({
mediaType: constants_1.CONTENT_TYPE_OCI_INDEX,
schemaVersion: 2,
manifests: [],
});
// Convert an image digest to a tag per the Referrers Tag Schema
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#referrers-tag-schema
const digestToTag = (digest) => {
return digest.replace(':', '-');
};
// Canonicalize the registry name to match the format used by the registry
// client. This is used primarily to handle the special case of the Docker Hub
// registry.
// https://github.com/moby/moby/blob/v24.0.2/registry/config.go#L25-L48
const canonicalizeRegistryName = (registry) => {
return registry.endsWith('docker.io') ? DOCKER_DEFAULT_REGISTRY : registry;
};
/***/ }),
/***/ 47353:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getImageDigest = exports.attachArtifactToImage = exports.OCIError = exports.getRegistryCredentials = void 0;
const image_1 = __nccwpck_require__(79539);
const name_1 = __nccwpck_require__(44520);
var credentials_1 = __nccwpck_require__(95475);
Object.defineProperty(exports, "getRegistryCredentials", ({ enumerable: true, get: function () { return credentials_1.getRegistryCredentials; } }));
var error_1 = __nccwpck_require__(60064);
Object.defineProperty(exports, "OCIError", ({ enumerable: true, get: function () { return error_1.OCIError; } }));
// Associates the given artifact with an OCI image. The artifact is identified
// by its media type and a buffer containing the artifact. The image is
// identified by its FQDN and digest.
const attachArtifactToImage = async (opts) => {
const image = (0, name_1.parseImageName)(opts.imageName);
return new image_1.OCIImage(image, opts.credentials, opts.fetchOpts).addArtifact(opts);
};
exports.attachArtifactToImage = attachArtifactToImage;
// Returns the digest of the given image tag in the remote registry.
const getImageDigest = async (opts) => {
const image = (0, name_1.parseImageName)(opts.imageName);
return new image_1.OCIImage(image, opts.credentials, opts.fetchOpts).getDigest(opts.imageTag);
};
exports.getImageDigest = getImageDigest;
/***/ }),
/***/ 44520:
/***/ ((__unused_webpack_module, exports) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.parseImageName = void 0;
const expression = (...res) => res.join('');
const group = (...res) => `(?:${expression(...res)})`;
const repeated = (...res) => `${group(expression(...res))}+`;
const optional = (...res) => `${group(expression(...res))}?`;
const capture = (...res) => `(${expression(...res)})`;
const anchored = (...res) => `^${expression(...res)}$`;
// Lower case letters, numbers
const ALPHA_NUMERIC_RE = '[a-z0-9]+';
// Separators allowed to be embedded in name components. This allows one period,
// one or two underscore or multiple dashes.
const SEPARATOR_RE = group('\\.|_|__|-+');
// Registry path component names to start with at least one letter or number,
// with following parts able to be separated by one period, one or two
// underscores or multiple dashes.
const NAME_COMPONENT_RE = expression(ALPHA_NUMERIC_RE, optional(repeated(SEPARATOR_RE, ALPHA_NUMERIC_RE)));
const NAME_RE = expression(NAME_COMPONENT_RE, repeated(optional('\\/', NAME_COMPONENT_RE)));
// Component of the registry domain must be at least one letter or number, with
// following parts able to be separated by a dash.
const DOMAIN_COMPONENT_RE = group('[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]');
// Restricts the registry domain to be one or more period separated components
// followed by an optional port.
const DOMAIN_RE = expression(DOMAIN_COMPONENT_RE, optional(repeated('\\.', DOMAIN_COMPONENT_RE)), optional(':[0-9]+'));
// Capture the registry domain and path components of a repository name.
const ANCHORED_NAME_RE = anchored(capture(DOMAIN_RE), '\\/', capture(NAME_RE));
// Parses a fully qualified image name into its registry and path components.
const parseImageName = (image) => {
const matches = image.match(ANCHORED_NAME_RE);
if (!matches) {
throw new Error(`Invalid image name: ${image}`);
}
return {
registry: matches[1],
path: matches[2],
};
};
exports.parseImageName = parseImageName;
/***/ }),
/***/ 27464:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var _RegistryClient_instances, _RegistryClient_baseURL, _RegistryClient_repository, _RegistryClient_fetch, _RegistryClient_fetchDistributionToken, _RegistryClient_fetchOAuth2Token;
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.RegistryClient = exports.ZERO_DIGEST = void 0;
/*
Copyright 2023 The Sigstore Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
const node_crypto_1 = __importDefault(__nccwpck_require__(6005));
const constants_1 = __nccwpck_require__(61319);
const credentials_1 = __nccwpck_require__(95475);
const error_1 = __nccwpck_require__(60064);
const fetch_1 = __importDefault(__nccwpck_require__(437));
const ALL_MANIFEST_MEDIA_TYPES = [
constants_1.CONTENT_TYPE_OCI_INDEX,
constants_1.CONTENT_TYPE_OCI_MANIFEST,
constants_1.CONTENT_TYPE_DOCKER_MANIFEST,
constants_1.CONTENT_TYPE_DOCKER_MANIFEST_LIST,
].join(',');
exports.ZERO_DIGEST = 'sha256:0000000000000000000000000000000000000000000000000000000000000000';
class RegistryClient {
constructor(registry, repository, opts) {
_RegistryClient_instances.add(this);
_RegistryClient_baseURL.set(this, void 0);
_RegistryClient_repository.set(this, void 0);
_RegistryClient_fetch.set(this, void 0);
__classPrivateFieldSet(this, _RegistryClient_repository, repository, "f");
__classPrivateFieldSet(this, _RegistryClient_fetch, fetch_1.default.defaults(opts), "f");
// Use http for localhost registries, https otherwise
const hostname = new URL(`http://${registry}`).hostname;
/* istanbul ignore next */
const protocol = hostname === 'localhost' || hostname === '127.0.0.1' ? 'http' : 'https';
__classPrivateFieldSet(this, _RegistryClient_baseURL, `${protocol}://${registry}`, "f");
}
// Authenticate with the registry. Sends an unauthenticated request to the
// registry in order to get an auth challenge. If the challenge scheme is
// "basic" we don't need a token and can authenticate requests using basic
// auth. Otherwise, we fetch a token from the auth server and use that to
// authenticate requests.
// https://github.com/google/go-containerregistry/blob/main/pkg/authn/README.md#the-registry
async signIn(creds) {
// Initiate a blob upload to get the auth challenge
const probeResponse = await __classPrivateFieldGet(this, _RegistryClient_fetch, "f").call(this, `${__classPrivateFieldGet(this, _RegistryClient_baseURL, "f")}/v2/${__classPrivateFieldGet(this, _RegistryClient_repository, "f")}/blobs/uploads/`, { method: 'POST' });
// If we get a 200 response, we're already authenticated
if (probeResponse.status === 200) {
return;
}
const authHeader = probeResponse.headers.get(constants_1.HEADER_AUTHENTICATE) ||
/* istanbul ignore next */ '';
const challenge = parseChallenge(authHeader);
// If the challenge scheme is "basic" we don't need a token and can
// authenticate requests using basic auth
if (challenge.scheme === 'basic') {
const basicAuth = (0, credentials_1.toBasicAuth)(creds);
__classPrivateFieldSet(this, _RegistryClient_fetch, __classPrivateFieldGet(this, _RegistryClient_fetch, "f").defaults({
headers: { [constants_1.HEADER_AUTHORIZATION]: `Basic ${basicAuth}` },
}), "f");
return;
}
let token;
if (creds.username === '<token>') {
// If the OAUth2 token request fails, try to fetch a distribution token
token = await __classPrivateFieldGet(this, _RegistryClient_instances, "m", _RegistryClient_fetchOAuth2Token).call(this, creds, challenge).catch(() => undefined);
}
if (!token) {
token = await __classPrivateFieldGet(this, _RegistryClient_instances, "m", _RegistryClient_fetchDistributionToken).call(this, creds, challenge);
}
// Ensure the token is sent with all future requests
__classPrivateFieldSet(this, _RegistryClient_fetch, __classPrivateFieldGet(this, _RegistryClient_fetch, "f").defaults({
headers: { [constants_1.HEADER_AUTHORIZATION]: `Bearer ${token}` },
}), "f");
}
// Check the registry API version
async checkVersion() {
const response = await __classPrivateFieldGet(this, _RegistryClient_fetch, "f").call(this, `${__classPrivateFieldGet(this, _RegistryClient_baseURL, "f")}/v2/`);
return response.headers.get(constants_1.HEADER_API_VERSION) || '';
}
// Upload a blob to the registry using the post/put method. Calculates the
// digest of the blob and checks to make sure the blob doesn't already exist
// in the registry before uploading.
async uploadBlob(blob) {
const digest = RegistryClient.digest(blob);
const size = blob.length;
// Check if blob already exists
const headResponse = await __classPrivateFieldGet(this, _RegistryClient_fetch, "f").call(this, `${__classPrivateFieldGet(this, _RegistryClient_baseURL, "f")}/v2/${__classPrivateFieldGet(this, _RegistryClient_repository, "f")}/blobs/${digest}`, { method: 'HEAD', redirect: 'follow' });
if (headResponse.status === 200) {
return {
mediaType: constants_1.CONTENT_TYPE_OCTET_STREAM,
digest,
size,
};
}
// Retrieve upload location (session ID)
const postResponse = await __classPrivateFieldGet(this, _RegistryClient_fetch, "f").call(this, `${__classPrivateFieldGet(this, _RegistryClient_baseURL, "f")}/v2/${__classPrivateFieldGet(this, _RegistryClient_repository, "f")}/blobs/uploads/`, { method: 'POST' }).then((0, error_1.ensureStatus)(202));
const location = postResponse.headers.get(constants_1.HEADER_LOCATION);
if (!location) {
throw new Error('Missing location for blob upload');
}
// Translate location to a full URL
const uploadLocation = new URL(location.startsWith('/') ? `${__classPrivateFieldGet(this, _RegistryClient_baseURL, "f")}${location}` : location);
// Add digest to query string
uploadLocation.searchParams.set('digest', digest);
// Upload blob
await __classPrivateFieldGet(this, _RegistryClient_fetch, "f").call(this, uploadLocation.href, {
method: 'PUT',
body: blob,
headers: { [constants_1.HEADER_CONTENT_TYPE]: constants_1.CONTENT_TYPE_OCTET_STREAM },
}).then((0, error_1.ensureStatus)(201));
return { mediaType: constants_1.CONTENT_TYPE_OCTET_STREAM, digest, size };
}
// Checks for the existence of a manifest by reference
async checkManifest(reference) {
const response = await __classPrivateFieldGet(this, _RegistryClient_fetch, "f").call(this, `${__classPrivateFieldGet(this, _RegistryClient_baseURL, "f")}/v2/${__classPrivateFieldGet(this, _RegistryClient_repository, "f")}/manifests/${reference}`, {
method: 'HEAD',
headers: { [constants_1.HEADER_ACCEPT]: ALL_MANIFEST_MEDIA_TYPES },
}).then((0, error_1.ensureStatus)(200));
const mediaType = response.headers.get(constants_1.HEADER_CONTENT_TYPE) ||
/* istanbul ignore next */ '';
const digest = response.headers.get(constants_1.HEADER_DIGEST) || /* istanbul ignore next */ '';
const size = Number(response.headers.get(constants_1.HEADER_CONTENT_LENGTH)) ||
/* istanbul ignore next */ 0;
return { mediaType, digest, size };
}
// Retrieves a manifest by reference
async getManifest(reference) {
const response = await __classPrivateFieldGet(this, _RegistryClient_fetch, "f").call(this, `${__classPrivateFieldGet(this, _RegistryClient_baseURL, "f")}/v2/${__classPrivateFieldGet(this, _RegistryClient_repository, "f")}/manifests/${reference}`, {
headers: { [constants_1.HEADER_ACCEPT]: ALL_MANIFEST_MEDIA_TYPES },
}).then((0, error_1.ensureStatus)(200));
const body = await response.json();
const mediaType = response.headers.get(constants_1.HEADER_CONTENT_TYPE) ||
/* istanbul ignore next */ '';
const digest = response.headers.get(constants_1.HEADER_DIGEST) || /* istanbul ignore next */ '';
const size = Number(response.headers.get(constants_1.HEADER_CONTENT_LENGTH)) || 0;
const etag = response.headers.get(constants_1.HEADER_ETAG) || undefined;
return { body, mediaType, digest, size, etag };
}
// Uploads a manifest by digest. If specified, the reference will be used as
// the manifest tag.
async uploadManifest(manifest, options = {}) {
const digest = RegistryClient.digest(manifest);
const reference = options.reference || digest;
const contentType = options.mediaType || constants_1.CONTENT_TYPE_OCI_MANIFEST;
const headers = { [constants_1.HEADER_CONTENT_TYPE]: contentType };
if (options.etag) {
headers[constants_1.HEADER_IF_MATCH] = options.etag;
}
const response = await __classPrivateFieldGet(this, _RegistryClient_fetch, "f").call(this, `${__classPrivateFieldGet(this, _RegistryClient_baseURL, "f")}/v2/${__classPrivateFieldGet(this, _RegistryClient_repository, "f")}/manifests/${reference}`, { method: 'PUT', body: manifest, headers }).then((0, error_1.ensureStatus)(201));
const subjectDigest = response.headers.get(constants_1.HEADER_OCI_SUBJECT) || undefined;
return {
mediaType: contentType,
digest,
size: manifest.length,
subjectDigest,
};
}
// Returns true if the registry supports the referrers API
async pingReferrers() {
const response = await __classPrivateFieldGet(this, _RegistryClient_fetch, "f").call(this, `${__classPrivateFieldGet(this, _RegistryClient_baseURL, "f")}/v2/${__classPrivateFieldGet(this, _RegistryClient_repository, "f")}/referrers/${exports.ZERO_DIGEST}`);
return response.status === 200;
}
static digest(blob) {
const hash = node_crypto_1.default.createHash('sha256');
hash.update(blob);
return `sha256:${hash.digest('hex')}`;
}
}
exports.RegistryClient = RegistryClient;
_RegistryClient_baseURL = new WeakMap(), _RegistryClient_repository = new WeakMap(), _RegistryClient_fetch = new WeakMap(), _RegistryClient_instances = new WeakSet(), _RegistryClient_fetchDistributionToken = async function _RegistryClient_fetchDistributionToken(creds, challenge) {
const basicAuth = (0, credentials_1.toBasicAuth)(creds);
const authURL = new URL(challenge.realm);
authURL.searchParams.set('service', challenge.service);
authURL.searchParams.set('scope', challenge.scope);
// Make token request with basic auth
const tokenResponse = await __classPrivateFieldGet(this, _RegistryClient_fetch, "f").call(this, authURL.toString(), {
headers: { [constants_1.HEADER_AUTHORIZATION]: `Basic ${basicAuth}` },
}).then((0, error_1.ensureStatus)(200));
return tokenResponse.json().then((json) => json.access_token || json.token);
}, _RegistryClient_fetchOAuth2Token = async function _RegistryClient_fetchOAuth2Token(creds, challenge) {
const body = new URLSearchParams({
service: challenge.service,
scope: challenge.scope,
username: creds.username,
password: creds.password,
grant_type: 'password',
});
// Make OAuth token request
const tokenResponse = await __classPrivateFieldGet(this, _RegistryClient_fetch, "f").call(this, challenge.realm, {
method: 'POST',
body,
}).then((0, error_1.ensureStatus)(200));
return tokenResponse.json().then((json) => json.access_token);
};
// Parses an auth challenge header into its components
// https://datatracker.ietf.org/doc/html/rfc7235#section-4.1
function parseChallenge(challenge) {
// Account for the possibility of spaces in the auth params
const [scheme, ...rest] = challenge.split(' ');
const authParams = rest.join(' ');
if (!['Basic', 'Bearer'].includes(scheme)) {
throw new Error(`Invalid challenge: ${challenge}`);
}
return {
scheme: scheme.toLocaleLowerCase(),
realm: singleMatch(authParams, /realm="(.+?)"/),
service: singleMatch(authParams, /service="(.+?)"/),
scope: singleMatch(authParams, /scope="(.+?)"/),
};
}
// Returns the first capture group of a regex match, or an empty string
const singleMatch = (str, regex) => str.match(regex)?.[1] || '';
/***/ }), /***/ }),
/***/ 70714: /***/ 70714:
@@ -106880,6 +107617,7 @@ const ociContainer = __importStar(__nccwpck_require__(33207));
const ghcr = __importStar(__nccwpck_require__(62894)); const ghcr = __importStar(__nccwpck_require__(62894));
const attest = __importStar(__nccwpck_require__(74113)); const attest = __importStar(__nccwpck_require__(74113));
const cfg = __importStar(__nccwpck_require__(96373)); const cfg = __importStar(__nccwpck_require__(96373));
const oci_1 = __nccwpck_require__(47353);
/** /**
* The main function for the action. * The main function for the action.
* @returns {Promise<void>} Resolves when the action is complete. * @returns {Promise<void>} Resolves when the action is complete.
@@ -106898,17 +107636,22 @@ async function run() {
const archives = await fsHelper.createArchives(stagedActionFilesDir, archiveDir); const archives = await fsHelper.createArchives(stagedActionFilesDir, archiveDir);
const manifest = ociContainer.createActionPackageManifest(archives.tarFile, archives.zipFile, options.nameWithOwner, options.repositoryId, options.repositoryOwnerId, options.sha, semverTag.raw, new Date()); const manifest = ociContainer.createActionPackageManifest(archives.tarFile, archives.zipFile, options.nameWithOwner, options.repositoryId, options.repositoryOwnerId, options.sha, semverTag.raw, new Date());
const manifestDigest = ociContainer.sha256Digest(manifest); const manifestDigest = ociContainer.sha256Digest(manifest);
// Attestations are not currently supported in GHES.
if (!options.isEnterprise) {
const attestation = await generateAttestation(manifestDigest, semverTag.raw, options);
if (attestation.attestationID !== undefined) {
core.setOutput('attestation-id', attestation.attestationID);
}
}
const { packageURL, publishedDigest } = await ghcr.publishOCIArtifact(options.token, options.containerRegistryUrl, options.nameWithOwner, semverTag.raw, archives.zipFile, archives.tarFile, manifest); const { packageURL, publishedDigest } = await ghcr.publishOCIArtifact(options.token, options.containerRegistryUrl, options.nameWithOwner, semverTag.raw, archives.zipFile, archives.tarFile, manifest);
if (manifestDigest !== publishedDigest) { if (manifestDigest !== publishedDigest) {
throw new Error(`Unexpected digest returned for manifest. Expected ${manifestDigest}, got ${publishedDigest}`); throw new Error(`Unexpected digest returned for manifest. Expected ${manifestDigest}, got ${publishedDigest}`);
} }
// Attestations are not currently supported in GHES.
if (!options.isEnterprise) {
const attestation = await uploadAttestation(publishedDigest, semverTag.raw, options);
if (attestation.digest !== undefined) {
core.info(`Uploaded attestation ${attestation.digest}`);
core.setOutput('attestation-manifest-sha', attestation.digest);
}
if (attestation.urls !== undefined && attestation.urls.length > 0) {
core.info(`Attestation URL: ${attestation.digest}`);
core.setOutput('attestation-url', attestation.urls[0]);
}
}
core.setOutput('package-url', packageURL.toString()); core.setOutput('package-url', packageURL.toString());
core.setOutput('package-manifest', JSON.stringify(manifest)); core.setOutput('package-manifest', JSON.stringify(manifest));
core.setOutput('package-manifest-sha', publishedDigest); core.setOutput('package-manifest-sha', publishedDigest);
@@ -106936,19 +107679,39 @@ function parseSemverTagFromRef(opts) {
} }
// Generate an attestation using the actions toolkit // Generate an attestation using the actions toolkit
// Subject name will contain the repo/package name and the tag name // Subject name will contain the repo/package name and the tag name
async function generateAttestation(manifestDigest, semverTag, options) { async function uploadAttestation(manifestDigest, semverTag, options) {
const OCI_TIMEOUT = 30000;
const OCI_RETRY = 3;
const PREDICATE_TYPE = 'https://slsa.dev/provenance/v1';
const subjectName = `${options.nameWithOwner}@${semverTag}`; const subjectName = `${options.nameWithOwner}@${semverTag}`;
const subjectDigest = removePrefix(manifestDigest, 'sha256:'); const subjectDigest = removePrefix(manifestDigest, 'sha256:');
core.info(`Generating attestation ${subjectName} for digest ${subjectDigest}`); core.info(`Generating attestation ${subjectName} for digest ${subjectDigest}`);
return await attest.attestProvenance({ const attestation = await attest.attestProvenance({
subjectName, subjectName,
subjectDigest: { sha256: subjectDigest }, subjectDigest: { sha256: subjectDigest },
token: options.token, token: options.token,
sigstore: 'github', sigstore: 'github',
// Always store the attestation using the GitHub Attestations API skipWrite: true // We will upload attestations to GHCR
skipWrite: false, });
// Identify the attestation to our API as an Immutable Action // Upload the attestation to the GitHub Container Registry
headers: { 'X-GitHub-Publish-Action': subjectName } const credentials = { username: 'token', password: options.token };
return await (0, oci_1.attachArtifactToImage)({
credentials,
imageName: `${options.containerRegistryUrl.host}/${options.nameWithOwner}`,
imageDigest: manifestDigest,
artifact: Buffer.from(JSON.stringify(attestation.bundle)),
mediaType: attestation.bundle.mediaType,
annotations: {
'dev.sigstore.bundle.content': 'dsse-envelope',
'dev.sigstore.bundle.predicateType': PREDICATE_TYPE,
'com.github.package.type': 'actions_oci_pkg_attestation'
},
fetchOpts: {
timeout: OCI_TIMEOUT,
retry: OCI_RETRY,
proxy: undefined,
noProxy: undefined
}
}); });
} }
function removePrefix(str, prefix) { function removePrefix(str, prefix) {
@@ -107259,6 +108022,14 @@ module.exports = require("node:https");
/***/ }), /***/ }),
/***/ 70612:
/***/ ((module) => {
"use strict";
module.exports = require("node:os");
/***/ }),
/***/ 49411: /***/ 49411:
/***/ ((module) => { /***/ ((module) => {
Generated Vendored
+3
View File
@@ -786,6 +786,9 @@ Apache-2.0
limitations under the License. limitations under the License.
@sigstore/oci
Apache-2.0
@sigstore/protobuf-specs @sigstore/protobuf-specs
Apache-2.0 Apache-2.0
+13
View File
@@ -13,6 +13,7 @@
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1", "@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0", "@actions/github": "^6.0.0",
"@sigstore/oci": "^0.3.7",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
@@ -1680,6 +1681,18 @@
"node": "^16.14.0 || >=18.0.0" "node": "^16.14.0 || >=18.0.0"
} }
}, },
"node_modules/@sigstore/oci": {
"version": "0.3.7",
"resolved": "https://registry.npmjs.org/@sigstore/oci/-/oci-0.3.7.tgz",
"integrity": "sha512-1JmebwEXil+NVzugFURbC+D3Vzj6WyTI1B+7damUk94dWXamE9cJ057iSo72rupiSozM6N7lVMjtD1c/P5Rrrw==",
"dependencies": {
"make-fetch-happen": "^13.0.1",
"proc-log": "^4.2.0"
},
"engines": {
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/@sigstore/protobuf-specs": { "node_modules/@sigstore/protobuf-specs": {
"version": "0.3.2", "version": "0.3.2",
"resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.2.tgz", "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.2.tgz",
+3 -2
View File
@@ -71,11 +71,12 @@
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1", "@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0", "@actions/github": "^6.0.0",
"@sigstore/oci": "^0.3.7",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"archiver": "^7.0.1", "archiver": "^7.0.1",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"tar": "^7.4.3", "simple-git": "^3.22.0",
"simple-git": "^3.22.0" "tar": "^7.4.3"
}, },
"devDependencies": { "devDependencies": {
"@types/archiver": "^6.0.2", "@types/archiver": "^6.0.2",
+48 -19
View File
@@ -5,6 +5,7 @@ import * as ociContainer from './oci-container'
import * as ghcr from './ghcr-client' import * as ghcr from './ghcr-client'
import * as attest from '@actions/attest' import * as attest from '@actions/attest'
import * as cfg from './config' import * as cfg from './config'
import { attachArtifactToImage, Descriptor } from '@sigstore/oci'
/** /**
* The main function for the action. * The main function for the action.
@@ -52,18 +53,6 @@ export async function run(): Promise<void> {
const manifestDigest = ociContainer.sha256Digest(manifest) const manifestDigest = ociContainer.sha256Digest(manifest)
// Attestations are not currently supported in GHES.
if (!options.isEnterprise) {
const attestation = await generateAttestation(
manifestDigest,
semverTag.raw,
options
)
if (attestation.attestationID !== undefined) {
core.setOutput('attestation-id', attestation.attestationID)
}
}
const { packageURL, publishedDigest } = await ghcr.publishOCIArtifact( const { packageURL, publishedDigest } = await ghcr.publishOCIArtifact(
options.token, options.token,
options.containerRegistryUrl, options.containerRegistryUrl,
@@ -80,6 +69,23 @@ export async function run(): Promise<void> {
) )
} }
// Attestations are not currently supported in GHES.
if (!options.isEnterprise) {
const attestation = await uploadAttestation(
publishedDigest,
semverTag.raw,
options
)
if (attestation.digest !== undefined) {
core.info(`Uploaded attestation ${attestation.digest}`)
core.setOutput('attestation-manifest-sha', attestation.digest)
}
if (attestation.urls !== undefined && attestation.urls.length > 0) {
core.info(`Attestation URL: ${attestation.digest}`)
core.setOutput('attestation-url', attestation.urls[0])
}
}
core.setOutput('package-url', packageURL.toString()) core.setOutput('package-url', packageURL.toString())
core.setOutput('package-manifest', JSON.stringify(manifest)) core.setOutput('package-manifest', JSON.stringify(manifest))
core.setOutput('package-manifest-sha', publishedDigest) core.setOutput('package-manifest-sha', publishedDigest)
@@ -112,25 +118,48 @@ function parseSemverTagFromRef(opts: cfg.PublishActionOptions): semver.SemVer {
// Generate an attestation using the actions toolkit // Generate an attestation using the actions toolkit
// Subject name will contain the repo/package name and the tag name // Subject name will contain the repo/package name and the tag name
async function generateAttestation( async function uploadAttestation(
manifestDigest: string, manifestDigest: string,
semverTag: string, semverTag: string,
options: cfg.PublishActionOptions options: cfg.PublishActionOptions
): Promise<attest.Attestation> { ): Promise<Descriptor> {
const OCI_TIMEOUT = 30000
const OCI_RETRY = 3
const PREDICATE_TYPE = 'https://slsa.dev/provenance/v1'
const subjectName = `${options.nameWithOwner}@${semverTag}` const subjectName = `${options.nameWithOwner}@${semverTag}`
const subjectDigest = removePrefix(manifestDigest, 'sha256:') const subjectDigest = removePrefix(manifestDigest, 'sha256:')
core.info(`Generating attestation ${subjectName} for digest ${subjectDigest}`) core.info(`Generating attestation ${subjectName} for digest ${subjectDigest}`)
return await attest.attestProvenance({ const attestation = await attest.attestProvenance({
subjectName, subjectName,
subjectDigest: { sha256: subjectDigest }, subjectDigest: { sha256: subjectDigest },
token: options.token, token: options.token,
sigstore: 'github', sigstore: 'github',
// Always store the attestation using the GitHub Attestations API skipWrite: true // We will upload attestations to GHCR
skipWrite: false, })
// Identify the attestation to our API as an Immutable Action
headers: { 'X-GitHub-Publish-Action': subjectName } // Upload the attestation to the GitHub Container Registry
const credentials = { username: 'token', password: options.token }
return await attachArtifactToImage({
credentials,
imageName: `${options.containerRegistryUrl.host}/${options.nameWithOwner}`,
imageDigest: manifestDigest,
artifact: Buffer.from(JSON.stringify(attestation.bundle)),
mediaType: attestation.bundle.mediaType,
annotations: {
'dev.sigstore.bundle.content': 'dsse-envelope',
'dev.sigstore.bundle.predicateType': PREDICATE_TYPE,
'com.github.package.type': 'actions_oci_pkg_attestation'
},
fetchOpts: {
timeout: OCI_TIMEOUT,
retry: OCI_RETRY,
proxy: undefined,
noProxy: undefined
}
}) })
} }