diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
index fbe21d3..652e3b6 100644
--- a/__tests__/main.test.ts
+++ b/__tests__/main.test.ts
@@ -13,6 +13,7 @@ import * as cfg from '../src/config'
import * as fsHelper from '../src/fs-helper'
import * as ghcr from '../src/ghcr-client'
import * as ociContainer from '../src/oci-container'
+import * as oci from '@sigstore/oci'
const ghcrUrl = new URL('https://ghcr.io')
@@ -38,6 +39,9 @@ let resolvePublishActionOptionsMock: jest.SpyInstance
// Mock generating attestation
let generateAttestationMock: jest.SpyInstance
+// Mock uploading attestation with oci lib
+let attachArtifactToImageMock: jest.SpyInstance
+
describe('run', () => {
beforeEach(() => {
jest.clearAllMocks()
@@ -79,6 +83,9 @@ describe('run', () => {
generateAttestationMock = jest
.spyOn(attest, 'attestProvenance')
.mockImplementation()
+ attachArtifactToImageMock = jest
+ .spyOn(oci, 'attachArtifactToImage')
+ .mockImplementation()
})
it('fails if the action ref is not a tag', async () => {
@@ -200,47 +207,6 @@ describe('run', () => {
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 () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
@@ -272,7 +238,7 @@ describe('run', () => {
})
generateAttestationMock.mockImplementation(async options => {
- expect(options).toHaveProperty('skipWrite', false)
+ expect(options).toHaveProperty('skipWrite', true)
return {
attestationID: 'test-attestation-id',
@@ -330,7 +296,7 @@ describe('run', () => {
})
generateAttestationMock.mockImplementation(async options => {
- expect(options).toHaveProperty('skipWrite', false)
+ expect(options).toHaveProperty('skipWrite', true)
return {
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 () => {
const options = baseOptions()
options.isEnterprise = true
@@ -464,7 +543,7 @@ describe('run', () => {
})
generateAttestationMock.mockImplementation(async options => {
- expect(options).toHaveProperty('skipWrite', false)
+ expect(options).toHaveProperty('skipWrite', true)
return {
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
await main.run()
@@ -487,7 +575,17 @@ describe('run', () => {
expect(publishOCIArtifactMock).toHaveBeenCalledTimes(1)
// 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(
'package-url',
@@ -503,11 +601,6 @@ describe('run', () => {
'package-manifest-sha',
'sha256:my-test-digest'
)
-
- expect(setOutputMock).toHaveBeenCalledWith(
- 'attestation-id',
- 'test-attestation-id'
- )
})
})
diff --git a/badges/coverage.svg b/badges/coverage.svg
index b75435b..c5876fe 100644
--- a/badges/coverage.svg
+++ b/badges/coverage.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/dist/index.js b/dist/index.js
index de11e31..4f139f9 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -52,7 +52,7 @@ function attest(options) {
// Store the attestation
let attestationID;
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);
});
@@ -249,10 +249,6 @@ const core_1 = __nccwpck_require__(42186);
const http_client_1 = __nccwpck_require__(96255);
const jose = __importStar(__nccwpck_require__(34061));
const OIDC_AUDIENCE = 'nobody';
-const VALID_SERVER_URLS = [
- 'https://github.com',
- new RegExp('^https://[a-z0-9-]+\\.ghe\\.com$')
-];
const REQUIRED_CLAIMS = [
'iss',
'ref',
@@ -268,7 +264,6 @@ const REQUIRED_CLAIMS = [
'run_attempt'
];
const getIDTokenClaims = (issuer) => __awaiter(void 0, void 0, void 0, function* () {
- issuer = issuer || getIssuer();
try {
const token = yield (0, core_1.getIDToken)(OIDC_AUDIENCE);
const claims = yield decodeOIDCToken(token, issuer);
@@ -312,19 +307,6 @@ function assertClaimSet(claims) {
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
/***/ }),
@@ -349,6 +331,7 @@ const attest_1 = __nccwpck_require__(46373);
const oidc_1 = __nccwpck_require__(95847);
const SLSA_PREDICATE_V1_TYPE = 'https://slsa.dev/provenance/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
* predicate using the GitHub Actions Workflow build type.
@@ -358,7 +341,7 @@ const GITHUB_BUILD_TYPE = 'https://actions.github.io/buildtypes/workflow/v1';
* issuer.
* @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 claims = yield (0, oidc_1.getIDTokenClaims)(issuer);
// 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, {
owner: github.context.repo.owner,
repo: github.context.repo.repo,
- headers: options.headers,
data: { bundle: attestation }
});
const data = typeof response.data == 'string'
@@ -11756,6 +11738,761 @@ class 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 === '') {
+ // 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:
@@ -106880,6 +107617,7 @@ const ociContainer = __importStar(__nccwpck_require__(33207));
const ghcr = __importStar(__nccwpck_require__(62894));
const attest = __importStar(__nccwpck_require__(74113));
const cfg = __importStar(__nccwpck_require__(96373));
+const oci_1 = __nccwpck_require__(47353);
/**
* The main function for the action.
* @returns {Promise} Resolves when the action is complete.
@@ -106898,17 +107636,22 @@ async function run() {
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 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);
if (manifestDigest !== 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-manifest', JSON.stringify(manifest));
core.setOutput('package-manifest-sha', publishedDigest);
@@ -106936,19 +107679,39 @@ function parseSemverTagFromRef(opts) {
}
// Generate an attestation using the actions toolkit
// 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 subjectDigest = removePrefix(manifestDigest, 'sha256:');
core.info(`Generating attestation ${subjectName} for digest ${subjectDigest}`);
- return await attest.attestProvenance({
+ const attestation = await attest.attestProvenance({
subjectName,
subjectDigest: { sha256: subjectDigest },
token: options.token,
sigstore: 'github',
- // Always store the attestation using the GitHub Attestations API
- skipWrite: false,
- // Identify the attestation to our API as an Immutable Action
- headers: { 'X-GitHub-Publish-Action': subjectName }
+ skipWrite: true // We will upload attestations to GHCR
+ });
+ // Upload the attestation to the GitHub Container Registry
+ 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) {
@@ -107259,6 +108022,14 @@ module.exports = require("node:https");
/***/ }),
+/***/ 70612:
+/***/ ((module) => {
+
+"use strict";
+module.exports = require("node:os");
+
+/***/ }),
+
/***/ 49411:
/***/ ((module) => {
diff --git a/dist/licenses.txt b/dist/licenses.txt
index cd594d7..d1bedc9 100644
--- a/dist/licenses.txt
+++ b/dist/licenses.txt
@@ -786,6 +786,9 @@ Apache-2.0
limitations under the License.
+@sigstore/oci
+Apache-2.0
+
@sigstore/protobuf-specs
Apache-2.0
diff --git a/package-lock.json b/package-lock.json
index 12663b9..560b92a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
+ "@sigstore/oci": "^0.3.7",
"@types/fs-extra": "^11.0.4",
"archiver": "^7.0.1",
"fs-extra": "^11.2.0",
@@ -1680,6 +1681,18 @@
"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": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.2.tgz",
diff --git a/package.json b/package.json
index 31af38a..77e25fc 100644
--- a/package.json
+++ b/package.json
@@ -71,11 +71,12 @@
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
+ "@sigstore/oci": "^0.3.7",
"@types/fs-extra": "^11.0.4",
"archiver": "^7.0.1",
"fs-extra": "^11.2.0",
- "tar": "^7.4.3",
- "simple-git": "^3.22.0"
+ "simple-git": "^3.22.0",
+ "tar": "^7.4.3"
},
"devDependencies": {
"@types/archiver": "^6.0.2",
diff --git a/src/main.ts b/src/main.ts
index a9fe215..f8180b2 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -5,6 +5,7 @@ import * as ociContainer from './oci-container'
import * as ghcr from './ghcr-client'
import * as attest from '@actions/attest'
import * as cfg from './config'
+import { attachArtifactToImage, Descriptor } from '@sigstore/oci'
/**
* The main function for the action.
@@ -52,18 +53,6 @@ export async function run(): Promise {
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,
@@ -80,6 +69,23 @@ export async function run(): Promise {
)
}
+ // 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-manifest', JSON.stringify(manifest))
core.setOutput('package-manifest-sha', publishedDigest)
@@ -112,25 +118,48 @@ function parseSemverTagFromRef(opts: cfg.PublishActionOptions): semver.SemVer {
// Generate an attestation using the actions toolkit
// Subject name will contain the repo/package name and the tag name
-async function generateAttestation(
+async function uploadAttestation(
manifestDigest: string,
semverTag: string,
options: cfg.PublishActionOptions
-): Promise {
+): Promise {
+ const OCI_TIMEOUT = 30000
+ const OCI_RETRY = 3
+ const PREDICATE_TYPE = 'https://slsa.dev/provenance/v1'
+
const subjectName = `${options.nameWithOwner}@${semverTag}`
const subjectDigest = removePrefix(manifestDigest, 'sha256:')
core.info(`Generating attestation ${subjectName} for digest ${subjectDigest}`)
- return await attest.attestProvenance({
+ const attestation = await attest.attestProvenance({
subjectName,
subjectDigest: { sha256: subjectDigest },
token: options.token,
sigstore: 'github',
- // Always store the attestation using the GitHub Attestations API
- skipWrite: false,
- // Identify the attestation to our API as an Immutable Action
- headers: { 'X-GitHub-Publish-Action': subjectName }
+ skipWrite: true // We will upload attestations to GHCR
+ })
+
+ // 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
+ }
})
}