upload attestation to GHCR instead of attestations API
This commit is contained in:
+143
-50
@@ -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
@@ -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 |
+805
-34
@@ -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) => {
|
||||||
|
|
||||||
|
|||||||
+3
@@ -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
|
||||||
|
|
||||||
|
|||||||
Generated
+13
@@ -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
@@ -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
@@ -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
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user