New subject-checksums input param (#198)
* new subject-checksums input param Signed-off-by: Brian DeHamer <[email protected]> * check for valid hex string for digest Signed-off-by: Brian DeHamer <[email protected]> --------- Signed-off-by: Brian DeHamer <[email protected]>
This commit is contained in:
@@ -74,20 +74,25 @@ See [action.yml](action.yml)
|
|||||||
- uses: actions/attest@v2
|
- uses: actions/attest@v2
|
||||||
with:
|
with:
|
||||||
# Path to the artifact serving as the subject of the attestation. Must
|
# Path to the artifact serving as the subject of the attestation. Must
|
||||||
# specify exactly one of "subject-path" or "subject-digest". May contain
|
# specify exactly one of "subject-path", "subject-digest", or
|
||||||
# a glob pattern or list of paths (total subject count cannot exceed 1024).
|
# "subject-checksums". May contain a glob pattern or list of paths
|
||||||
|
# (total subject count cannot exceed 1024).
|
||||||
subject-path:
|
subject-path:
|
||||||
|
|
||||||
# SHA256 digest of the subject for the attestation. Must be in the form
|
# SHA256 digest of the subject for the attestation. Must be in the form
|
||||||
# "sha256:hex_digest" (e.g. "sha256:abc123..."). Must specify exactly one
|
# "sha256:hex_digest" (e.g. "sha256:abc123..."). Must specify exactly one
|
||||||
# of "subject-path" or "subject-digest".
|
# of "subject-path", "subject-digest", or "subject-checksums".
|
||||||
subject-digest:
|
subject-digest:
|
||||||
|
|
||||||
# Subject name as it should appear in the attestation. Required unless
|
# Subject name as it should appear in the attestation. Required when
|
||||||
# "subject-path" is specified, in which case it will be inferred from the
|
# identifying the subject with the "subject-digest" input.
|
||||||
# path.
|
|
||||||
subject-name:
|
subject-name:
|
||||||
|
|
||||||
|
# Path to checksums file containing digest and name of subjects for
|
||||||
|
# attestation. Must specify exactly one of "subject-path", "subject-digest",
|
||||||
|
# or "subject-checksums".
|
||||||
|
subject-checksums:
|
||||||
|
|
||||||
# URI identifying the type of the predicate.
|
# URI identifying the type of the predicate.
|
||||||
predicate-type:
|
predicate-type:
|
||||||
|
|
||||||
@@ -209,6 +214,43 @@ newline delimited list:
|
|||||||
dist/bar
|
dist/bar
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Identify Subjects with Checksums File
|
||||||
|
|
||||||
|
If you are using tools like
|
||||||
|
[goreleaser](https://goreleaser.com/customization/checksum/) or
|
||||||
|
[jreleaser](https://jreleaser.org/guide/latest/reference/checksum.html) which
|
||||||
|
generate a checksums file you can identify the attestation subjects by passing
|
||||||
|
the path of the checksums file to the `subject-checksums` input. Each of the
|
||||||
|
artifacts identified in the checksums file will be listed as a subject for the
|
||||||
|
attestation.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Calculate artifact digests
|
||||||
|
run: |
|
||||||
|
shasum -a 256 foo_0.0.1_* > subject.checksums.txt
|
||||||
|
|
||||||
|
- uses: actions/attest@v2
|
||||||
|
with:
|
||||||
|
subject-checksums: subject.checksums.txt
|
||||||
|
predicate-type: 'https://example.com/predicate/v1'
|
||||||
|
predicate: '{}'
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- markdownlint-disable MD038 -->
|
||||||
|
|
||||||
|
The file referenced by the `subject-checksums` input must conform to the same
|
||||||
|
format used by the shasum tools. Each subject should be listed on a separate
|
||||||
|
line including the hex-encoded digest (either SHA256 or SHA512), a space, a
|
||||||
|
single character flag indicating either binary (`*`) or text (` `) input mode,
|
||||||
|
and the filename.
|
||||||
|
|
||||||
|
<!-- markdownlint-enable MD038 -->
|
||||||
|
|
||||||
|
```text
|
||||||
|
b569bf992b287f55d78bf8ee476497e9b7e9d2bf1c338860bfb905016218c740 foo_0.0.1_darwin_amd64
|
||||||
|
a54fc515e616cac7fcf11a49d5c5ec9ec315948a5935c1e11dd610b834b14dde foo_0.0.1_darwin_arm64
|
||||||
|
```
|
||||||
|
|
||||||
### Container Image
|
### Container Image
|
||||||
|
|
||||||
When working with container images you can invoke the action with the
|
When working with container images you can invoke the action with the
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const defaultInputs: main.RunInputs = {
|
|||||||
subjectName: '',
|
subjectName: '',
|
||||||
subjectDigest: '',
|
subjectDigest: '',
|
||||||
subjectPath: '',
|
subjectPath: '',
|
||||||
|
subjectChecksums: '',
|
||||||
pushToRegistry: false,
|
pushToRegistry: false,
|
||||||
showSummary: true,
|
showSummary: true,
|
||||||
githubToken: '',
|
githubToken: '',
|
||||||
@@ -138,7 +139,9 @@ describe('action', () => {
|
|||||||
|
|
||||||
expect(runMock).toHaveReturned()
|
expect(runMock).toHaveReturned()
|
||||||
expect(setFailedMock).toHaveBeenCalledWith(
|
expect(setFailedMock).toHaveBeenCalledWith(
|
||||||
new Error('One of subject-path or subject-digest must be provided')
|
new Error(
|
||||||
|
'One of subject-path, subject-digest, or subject-checksums must be provided'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+148
-4
@@ -12,13 +12,14 @@ describe('subjectFromInputs', () => {
|
|||||||
const blankInputs: SubjectInputs = {
|
const blankInputs: SubjectInputs = {
|
||||||
subjectPath: '',
|
subjectPath: '',
|
||||||
subjectName: '',
|
subjectName: '',
|
||||||
subjectDigest: ''
|
subjectDigest: '',
|
||||||
|
subjectChecksums: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('when no inputs are provided', () => {
|
describe('when no inputs are provided', () => {
|
||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(subjectFromInputs(blankInputs)).rejects.toThrow(
|
await expect(subjectFromInputs(blankInputs)).rejects.toThrow(
|
||||||
/one of subject-path or subject-digest must be provided/i
|
/one of subject-path, subject-digest, or subject-checksums must be provided/i
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -28,11 +29,42 @@ describe('subjectFromInputs', () => {
|
|||||||
const inputs: SubjectInputs = {
|
const inputs: SubjectInputs = {
|
||||||
subjectName: 'foo',
|
subjectName: 'foo',
|
||||||
subjectPath: 'path/to/subject',
|
subjectPath: 'path/to/subject',
|
||||||
subjectDigest: 'digest'
|
subjectDigest: 'digest',
|
||||||
|
subjectChecksums: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
||||||
/only one of subject-path or subject-digest may be provided/i
|
/only one of subject-path, subject-digest, or subject-checksums may be provided/i
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when both subject path and subject checksums are provided', () => {
|
||||||
|
it('throws an error', async () => {
|
||||||
|
const inputs: SubjectInputs = {
|
||||||
|
subjectName: '',
|
||||||
|
subjectPath: 'path/to/subject',
|
||||||
|
subjectDigest: '',
|
||||||
|
subjectChecksums: 'path/to/checksums'
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
||||||
|
/only one of subject-path, subject-digest, or subject-checksums may be provided/i
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when both subject digest and subject checksums are provided', () => {
|
||||||
|
it('throws an error', async () => {
|
||||||
|
const inputs: SubjectInputs = {
|
||||||
|
subjectName: 'foo',
|
||||||
|
subjectPath: '',
|
||||||
|
subjectDigest: 'digest',
|
||||||
|
subjectChecksums: 'path/to/checksums'
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
||||||
|
/only one of subject-path, subject-digest, or subject-checksums may be provided/i
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -387,6 +419,118 @@ describe('subjectFromInputs', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('when specifying a subject checksums file', () => {
|
||||||
|
const checksums = `
|
||||||
|
187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d demo_0.0.1_linux_amd64
|
||||||
|
badline
|
||||||
|
5d8b4751ef31f9440d843fcfa4e53ca2e25b1cb1f13fd355fdc7c24b41fe645293291ea9297ba3989078abb77ebbaac66be073618a9e4974dbd0361881d4c718 demo_0.0.1_darwin_arm64`
|
||||||
|
|
||||||
|
let dir = ''
|
||||||
|
const filename = 'checksums'
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Set-up temp directory
|
||||||
|
const tmpDir = await fs.realpath(os.tmpdir())
|
||||||
|
dir = await fs.mkdtemp(tmpDir + path.sep)
|
||||||
|
|
||||||
|
// Write file to temp directory
|
||||||
|
await fs.writeFile(path.join(dir, filename), checksums)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
// Clean-up temp directory
|
||||||
|
await fs.rm(dir, { recursive: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the specified path is NOT a file', () => {
|
||||||
|
it('throws an error', async () => {
|
||||||
|
const inputs: SubjectInputs = {
|
||||||
|
...blankInputs,
|
||||||
|
subjectChecksums: dir
|
||||||
|
}
|
||||||
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
||||||
|
/subject checksums file not found/i
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when the specific path is a file', () => {
|
||||||
|
it('returns the multiple subjects', async () => {
|
||||||
|
const inputs: SubjectInputs = {
|
||||||
|
...blankInputs,
|
||||||
|
subjectChecksums: path.join(dir, filename)
|
||||||
|
}
|
||||||
|
const subjects = await subjectFromInputs(inputs)
|
||||||
|
|
||||||
|
expect(subjects).toBeDefined()
|
||||||
|
expect(subjects).toHaveLength(2)
|
||||||
|
|
||||||
|
expect(subjects).toContainEqual({
|
||||||
|
name: 'demo_0.0.1_linux_amd64',
|
||||||
|
digest: {
|
||||||
|
sha256:
|
||||||
|
'187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when specifying a subject checksums string', () => {
|
||||||
|
const checksums = `
|
||||||
|
f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386
|
||||||
|
187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d demo_0.0.1_linux_amd64
|
||||||
|
9ecbf449e286a8a8748c161c52aa28b6b2fc64ab86f94161c5d1b3abc18156c5 demo_0.0.1_linux_arm64`
|
||||||
|
|
||||||
|
it('returns the multiple subjects', async () => {
|
||||||
|
const inputs: SubjectInputs = {
|
||||||
|
...blankInputs,
|
||||||
|
subjectChecksums: checksums
|
||||||
|
}
|
||||||
|
const subjects = await subjectFromInputs(inputs)
|
||||||
|
|
||||||
|
expect(subjects).toBeDefined()
|
||||||
|
expect(subjects).toHaveLength(3)
|
||||||
|
|
||||||
|
expect(subjects).toContainEqual({
|
||||||
|
name: 'demo_0.0.1_linux_386',
|
||||||
|
digest: {
|
||||||
|
sha256:
|
||||||
|
'f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when specifying a subject checksums string with an unrecognized digest', () => {
|
||||||
|
const checksums = `f861e demo_0.0.1_linux_386`
|
||||||
|
|
||||||
|
it('throws an error', async () => {
|
||||||
|
const inputs: SubjectInputs = {
|
||||||
|
...blankInputs,
|
||||||
|
subjectChecksums: checksums
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(
|
||||||
|
/unknown digest algorithm/i
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('when specifying a subject checksums string with an invalid digest', () => {
|
||||||
|
const checksums =
|
||||||
|
'!!!!e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386'
|
||||||
|
|
||||||
|
it('throws an error', async () => {
|
||||||
|
const inputs: SubjectInputs = {
|
||||||
|
...blankInputs,
|
||||||
|
subjectChecksums: checksums
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(subjectFromInputs(inputs)).rejects.toThrow(/invalid digest/i)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('subjectDigest', () => {
|
describe('subjectDigest', () => {
|
||||||
|
|||||||
+12
-6
@@ -9,20 +9,26 @@ inputs:
|
|||||||
subject-path:
|
subject-path:
|
||||||
description: >
|
description: >
|
||||||
Path to the artifact serving as the subject of the attestation. Must
|
Path to the artifact serving as the subject of the attestation. Must
|
||||||
specify exactly one of "subject-path" or "subject-digest". May contain a
|
specify exactly one of "subject-path", "subject-digest", or
|
||||||
glob pattern or list of paths (total subject count cannot exceed 1024).
|
"subject-checksums". May contain a glob pattern or list of paths (total
|
||||||
|
subject count cannot exceed 1024).
|
||||||
required: false
|
required: false
|
||||||
subject-digest:
|
subject-digest:
|
||||||
description: >
|
description: >
|
||||||
Digest of the subject for the attestation. Must be in the form
|
Digest of the subject for the attestation. Must be in the form
|
||||||
"algorithm:hex_digest" (e.g. "sha256:abc123..."). Must specify exactly one
|
"algorithm:hex_digest" (e.g. "sha256:abc123..."). Must specify exactly one
|
||||||
of "subject-path" or "subject-digest".
|
of "subject-path", "subject-digest", or "subject-checksums".
|
||||||
required: false
|
required: false
|
||||||
subject-name:
|
subject-name:
|
||||||
description: >
|
description: >
|
||||||
Subject name as it should appear in the attestation. Required unless
|
Subject name as it should appear in the attestation. Required when
|
||||||
"subject-path" is specified, in which case it will be inferred from the
|
identifying the subject with the "subject-digest" input.
|
||||||
path.
|
required: false
|
||||||
|
subject-checksums:
|
||||||
|
description: >
|
||||||
|
Path to checksums file containing digest and name of subjects for
|
||||||
|
attestation. Must specify exactly one of "subject-path", "subject-digest",
|
||||||
|
or "subject-checksums".
|
||||||
required: false
|
required: false
|
||||||
predicate-type:
|
predicate-type:
|
||||||
description: >
|
description: >
|
||||||
|
|||||||
+77
-12
@@ -70835,6 +70835,7 @@ const inputs = {
|
|||||||
subjectPath: core.getInput('subject-path'),
|
subjectPath: core.getInput('subject-path'),
|
||||||
subjectName: core.getInput('subject-name'),
|
subjectName: core.getInput('subject-name'),
|
||||||
subjectDigest: core.getInput('subject-digest'),
|
subjectDigest: core.getInput('subject-digest'),
|
||||||
|
subjectChecksums: core.getInput('subject-checksums'),
|
||||||
predicateType: core.getInput('predicate-type'),
|
predicateType: core.getInput('predicate-type'),
|
||||||
predicate: core.getInput('predicate'),
|
predicate: core.getInput('predicate'),
|
||||||
predicatePath: core.getInput('predicate-path'),
|
predicatePath: core.getInput('predicate-path'),
|
||||||
@@ -71129,23 +71130,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
exports.formatSubjectDigest = exports.subjectFromInputs = void 0;
|
exports.formatSubjectDigest = exports.subjectFromInputs = void 0;
|
||||||
const glob = __importStar(__nccwpck_require__(47206));
|
const glob = __importStar(__nccwpck_require__(47206));
|
||||||
|
const assert_1 = __importDefault(__nccwpck_require__(42613));
|
||||||
const crypto_1 = __importDefault(__nccwpck_require__(76982));
|
const crypto_1 = __importDefault(__nccwpck_require__(76982));
|
||||||
const sync_1 = __nccwpck_require__(61110);
|
const sync_1 = __nccwpck_require__(61110);
|
||||||
const fs_1 = __importDefault(__nccwpck_require__(79896));
|
const fs_1 = __importDefault(__nccwpck_require__(79896));
|
||||||
|
const os_1 = __importDefault(__nccwpck_require__(70857));
|
||||||
const path_1 = __importDefault(__nccwpck_require__(16928));
|
const path_1 = __importDefault(__nccwpck_require__(16928));
|
||||||
const MAX_SUBJECT_COUNT = 1024;
|
const MAX_SUBJECT_COUNT = 1024;
|
||||||
|
const MAX_SUBJECT_CHECKSUM_SIZE_BYTES = 512 * MAX_SUBJECT_COUNT;
|
||||||
const DIGEST_ALGORITHM = 'sha256';
|
const DIGEST_ALGORITHM = 'sha256';
|
||||||
|
const HEX_STRING_RE = /^[0-9a-fA-F]+$/;
|
||||||
// Returns the subject specified by the action's inputs. The subject may be
|
// Returns the subject specified by the action's inputs. The subject may be
|
||||||
// specified as a path to a file or as a digest. If a path is provided, the
|
// specified as a path to a file or as a digest. If a path is provided, the
|
||||||
// file's digest is calculated and returned along with the subject's name. If a
|
// file's digest is calculated and returned along with the subject's name. If a
|
||||||
// digest is provided, the name must also be provided.
|
// digest is provided, the name must also be provided.
|
||||||
const subjectFromInputs = async (inputs) => {
|
const subjectFromInputs = async (inputs) => {
|
||||||
const { subjectPath, subjectDigest, subjectName, downcaseName } = inputs;
|
const { subjectPath, subjectDigest, subjectName, subjectChecksums, downcaseName } = inputs;
|
||||||
if (!subjectPath && !subjectDigest) {
|
const enabledInputs = [subjectPath, subjectDigest, subjectChecksums].filter(Boolean);
|
||||||
throw new Error('One of subject-path or subject-digest must be provided');
|
if (enabledInputs.length === 0) {
|
||||||
|
throw new Error('One of subject-path, subject-digest, or subject-checksums must be provided');
|
||||||
}
|
}
|
||||||
if (subjectPath && subjectDigest) {
|
if (enabledInputs.length > 1) {
|
||||||
throw new Error('Only one of subject-path or subject-digest may be provided');
|
throw new Error('Only one of subject-path, subject-digest, or subject-checksums may be provided');
|
||||||
}
|
}
|
||||||
if (subjectDigest && !subjectName) {
|
if (subjectDigest && !subjectName) {
|
||||||
throw new Error('subject-name must be provided when using subject-digest');
|
throw new Error('subject-name must be provided when using subject-digest');
|
||||||
@@ -71153,11 +71159,17 @@ const subjectFromInputs = async (inputs) => {
|
|||||||
// If push-to-registry is enabled, ensure the subject name is lowercase
|
// If push-to-registry is enabled, ensure the subject name is lowercase
|
||||||
// to conform to OCI image naming conventions
|
// to conform to OCI image naming conventions
|
||||||
const name = downcaseName ? subjectName.toLowerCase() : subjectName;
|
const name = downcaseName ? subjectName.toLowerCase() : subjectName;
|
||||||
if (subjectPath) {
|
switch (true) {
|
||||||
return await getSubjectFromPath(subjectPath, name);
|
case !!subjectPath:
|
||||||
}
|
return getSubjectFromPath(subjectPath, name);
|
||||||
else {
|
case !!subjectDigest:
|
||||||
return [getSubjectFromDigest(subjectDigest, name)];
|
return [getSubjectFromDigest(subjectDigest, name)];
|
||||||
|
case !!subjectChecksums:
|
||||||
|
return getSubjectFromChecksums(subjectChecksums);
|
||||||
|
/* istanbul ignore next */
|
||||||
|
default:
|
||||||
|
// This should be unreachable, but TS requires a default case
|
||||||
|
assert_1.default.fail('unreachable');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
exports.subjectFromInputs = subjectFromInputs;
|
exports.subjectFromInputs = subjectFromInputs;
|
||||||
@@ -71173,7 +71185,7 @@ exports.formatSubjectDigest = formatSubjectDigest;
|
|||||||
const getSubjectFromPath = async (subjectPath, subjectName) => {
|
const getSubjectFromPath = async (subjectPath, subjectName) => {
|
||||||
const digestedSubjects = [];
|
const digestedSubjects = [];
|
||||||
// Parse the list of subject paths
|
// Parse the list of subject paths
|
||||||
const subjectPaths = parseList(subjectPath).join('\n');
|
const subjectPaths = parseSubjectPathList(subjectPath).join('\n');
|
||||||
// Expand the globbed paths to a list of actual paths
|
// Expand the globbed paths to a list of actual paths
|
||||||
const paths = await glob.create(subjectPaths).then(async (g) => g.glob());
|
const paths = await glob.create(subjectPaths).then(async (g) => g.glob());
|
||||||
// Filter path list to just the files (not directories)
|
// Filter path list to just the files (not directories)
|
||||||
@@ -71206,6 +71218,49 @@ const getSubjectFromDigest = (subjectDigest, subjectName) => {
|
|||||||
digest: { [alg]: digest }
|
digest: { [alg]: digest }
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
const getSubjectFromChecksums = (subjectChecksums) => {
|
||||||
|
if (fs_1.default.existsSync(subjectChecksums)) {
|
||||||
|
return getSubjectFromChecksumsFile(subjectChecksums);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return getSubjectFromChecksumsString(subjectChecksums);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getSubjectFromChecksumsFile = (checksumsPath) => {
|
||||||
|
const stats = fs_1.default.statSync(checksumsPath);
|
||||||
|
if (!stats.isFile()) {
|
||||||
|
throw new Error(`subject checksums file not found: ${checksumsPath}`);
|
||||||
|
}
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (stats.size > MAX_SUBJECT_CHECKSUM_SIZE_BYTES) {
|
||||||
|
throw new Error(`subject checksums file exceeds maximum allowed size: ${MAX_SUBJECT_CHECKSUM_SIZE_BYTES} bytes`);
|
||||||
|
}
|
||||||
|
const checksums = fs_1.default.readFileSync(checksumsPath, 'utf-8');
|
||||||
|
return getSubjectFromChecksumsString(checksums);
|
||||||
|
};
|
||||||
|
const getSubjectFromChecksumsString = (checksums) => {
|
||||||
|
const subjects = [];
|
||||||
|
const records = checksums.split(os_1.default.EOL).filter(Boolean);
|
||||||
|
for (const record of records) {
|
||||||
|
// Find the space delimiter following the digest
|
||||||
|
const delimIndex = record.indexOf(' ');
|
||||||
|
// Skip any line that doesn't have a delimiter
|
||||||
|
if (delimIndex === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Swallow the type identifier character at the beginning of the name
|
||||||
|
const name = record.slice(delimIndex + 2);
|
||||||
|
const digest = record.slice(0, delimIndex);
|
||||||
|
if (!HEX_STRING_RE.test(digest)) {
|
||||||
|
throw new Error(`Invalid digest: ${digest}`);
|
||||||
|
}
|
||||||
|
subjects.push({
|
||||||
|
name,
|
||||||
|
digest: { [digestAlgorithm(digest)]: digest }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return subjects;
|
||||||
|
};
|
||||||
// Calculates the digest of a file using the specified algorithm. The file is
|
// Calculates the digest of a file using the specified algorithm. The file is
|
||||||
// streamed into the digest function to avoid loading the entire file into
|
// streamed into the digest function to avoid loading the entire file into
|
||||||
// memory. The returned digest is a hex string.
|
// memory. The returned digest is a hex string.
|
||||||
@@ -71218,7 +71273,7 @@ const digestFile = async (algorithm, filePath) => {
|
|||||||
.once('finish', () => resolve(hash.read()));
|
.once('finish', () => resolve(hash.read()));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const parseList = (input) => {
|
const parseSubjectPathList = (input) => {
|
||||||
const res = [];
|
const res = [];
|
||||||
const records = (0, sync_1.parse)(input, {
|
const records = (0, sync_1.parse)(input, {
|
||||||
columns: false,
|
columns: false,
|
||||||
@@ -71231,6 +71286,16 @@ const parseList = (input) => {
|
|||||||
}
|
}
|
||||||
return res.filter(item => item).map(pat => pat.trim());
|
return res.filter(item => item).map(pat => pat.trim());
|
||||||
};
|
};
|
||||||
|
const digestAlgorithm = (digest) => {
|
||||||
|
switch (digest.length) {
|
||||||
|
case 64:
|
||||||
|
return 'sha256';
|
||||||
|
case 128:
|
||||||
|
return 'sha512';
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown digest algorithm: ${digest}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const inputs: RunInputs = {
|
|||||||
subjectPath: core.getInput('subject-path'),
|
subjectPath: core.getInput('subject-path'),
|
||||||
subjectName: core.getInput('subject-name'),
|
subjectName: core.getInput('subject-name'),
|
||||||
subjectDigest: core.getInput('subject-digest'),
|
subjectDigest: core.getInput('subject-digest'),
|
||||||
|
subjectChecksums: core.getInput('subject-checksums'),
|
||||||
predicateType: core.getInput('predicate-type'),
|
predicateType: core.getInput('predicate-type'),
|
||||||
predicate: core.getInput('predicate'),
|
predicate: core.getInput('predicate'),
|
||||||
predicatePath: core.getInput('predicate-path'),
|
predicatePath: core.getInput('predicate-path'),
|
||||||
|
|||||||
+101
-11
@@ -1,18 +1,23 @@
|
|||||||
import * as glob from '@actions/glob'
|
import * as glob from '@actions/glob'
|
||||||
|
import assert from 'assert'
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
import { parse } from 'csv-parse/sync'
|
import { parse } from 'csv-parse/sync'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import os from 'os'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
|
||||||
import type { Subject } from '@actions/attest'
|
import type { Subject } from '@actions/attest'
|
||||||
|
|
||||||
const MAX_SUBJECT_COUNT = 1024
|
const MAX_SUBJECT_COUNT = 1024
|
||||||
|
const MAX_SUBJECT_CHECKSUM_SIZE_BYTES = 512 * MAX_SUBJECT_COUNT
|
||||||
const DIGEST_ALGORITHM = 'sha256'
|
const DIGEST_ALGORITHM = 'sha256'
|
||||||
|
const HEX_STRING_RE = /^[0-9a-fA-F]+$/
|
||||||
|
|
||||||
export type SubjectInputs = {
|
export type SubjectInputs = {
|
||||||
subjectPath: string
|
subjectPath: string
|
||||||
subjectName: string
|
subjectName: string
|
||||||
subjectDigest: string
|
subjectDigest: string
|
||||||
|
subjectChecksums: string
|
||||||
downcaseName?: boolean
|
downcaseName?: boolean
|
||||||
}
|
}
|
||||||
// Returns the subject specified by the action's inputs. The subject may be
|
// Returns the subject specified by the action's inputs. The subject may be
|
||||||
@@ -22,15 +27,26 @@ export type SubjectInputs = {
|
|||||||
export const subjectFromInputs = async (
|
export const subjectFromInputs = async (
|
||||||
inputs: SubjectInputs
|
inputs: SubjectInputs
|
||||||
): Promise<Subject[]> => {
|
): Promise<Subject[]> => {
|
||||||
const { subjectPath, subjectDigest, subjectName, downcaseName } = inputs
|
const {
|
||||||
|
subjectPath,
|
||||||
|
subjectDigest,
|
||||||
|
subjectName,
|
||||||
|
subjectChecksums,
|
||||||
|
downcaseName
|
||||||
|
} = inputs
|
||||||
|
|
||||||
if (!subjectPath && !subjectDigest) {
|
const enabledInputs = [subjectPath, subjectDigest, subjectChecksums].filter(
|
||||||
throw new Error('One of subject-path or subject-digest must be provided')
|
Boolean
|
||||||
|
)
|
||||||
|
if (enabledInputs.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
'One of subject-path, subject-digest, or subject-checksums must be provided'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subjectPath && subjectDigest) {
|
if (enabledInputs.length > 1) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Only one of subject-path or subject-digest may be provided'
|
'Only one of subject-path, subject-digest, or subject-checksums may be provided'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,10 +58,17 @@ export const subjectFromInputs = async (
|
|||||||
// to conform to OCI image naming conventions
|
// to conform to OCI image naming conventions
|
||||||
const name = downcaseName ? subjectName.toLowerCase() : subjectName
|
const name = downcaseName ? subjectName.toLowerCase() : subjectName
|
||||||
|
|
||||||
if (subjectPath) {
|
switch (true) {
|
||||||
return await getSubjectFromPath(subjectPath, name)
|
case !!subjectPath:
|
||||||
} else {
|
return getSubjectFromPath(subjectPath, name)
|
||||||
return [getSubjectFromDigest(subjectDigest, name)]
|
case !!subjectDigest:
|
||||||
|
return [getSubjectFromDigest(subjectDigest, name)]
|
||||||
|
case !!subjectChecksums:
|
||||||
|
return getSubjectFromChecksums(subjectChecksums)
|
||||||
|
/* istanbul ignore next */
|
||||||
|
default:
|
||||||
|
// This should be unreachable, but TS requires a default case
|
||||||
|
assert.fail('unreachable')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +88,7 @@ const getSubjectFromPath = async (
|
|||||||
const digestedSubjects: Subject[] = []
|
const digestedSubjects: Subject[] = []
|
||||||
|
|
||||||
// Parse the list of subject paths
|
// Parse the list of subject paths
|
||||||
const subjectPaths = parseList(subjectPath).join('\n')
|
const subjectPaths = parseSubjectPathList(subjectPath).join('\n')
|
||||||
|
|
||||||
// Expand the globbed paths to a list of actual paths
|
// Expand the globbed paths to a list of actual paths
|
||||||
const paths = await glob.create(subjectPaths).then(async g => g.glob())
|
const paths = await glob.create(subjectPaths).then(async g => g.glob())
|
||||||
@@ -119,6 +142,62 @@ const getSubjectFromDigest = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSubjectFromChecksums = (subjectChecksums: string): Subject[] => {
|
||||||
|
if (fs.existsSync(subjectChecksums)) {
|
||||||
|
return getSubjectFromChecksumsFile(subjectChecksums)
|
||||||
|
} else {
|
||||||
|
return getSubjectFromChecksumsString(subjectChecksums)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSubjectFromChecksumsFile = (checksumsPath: string): Subject[] => {
|
||||||
|
const stats = fs.statSync(checksumsPath)
|
||||||
|
if (!stats.isFile()) {
|
||||||
|
throw new Error(`subject checksums file not found: ${checksumsPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
|
if (stats.size > MAX_SUBJECT_CHECKSUM_SIZE_BYTES) {
|
||||||
|
throw new Error(
|
||||||
|
`subject checksums file exceeds maximum allowed size: ${MAX_SUBJECT_CHECKSUM_SIZE_BYTES} bytes`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const checksums = fs.readFileSync(checksumsPath, 'utf-8')
|
||||||
|
return getSubjectFromChecksumsString(checksums)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSubjectFromChecksumsString = (checksums: string): Subject[] => {
|
||||||
|
const subjects: Subject[] = []
|
||||||
|
|
||||||
|
const records: string[] = checksums.split(os.EOL).filter(Boolean)
|
||||||
|
|
||||||
|
for (const record of records) {
|
||||||
|
// Find the space delimiter following the digest
|
||||||
|
const delimIndex = record.indexOf(' ')
|
||||||
|
|
||||||
|
// Skip any line that doesn't have a delimiter
|
||||||
|
if (delimIndex === -1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swallow the type identifier character at the beginning of the name
|
||||||
|
const name = record.slice(delimIndex + 2)
|
||||||
|
const digest = record.slice(0, delimIndex)
|
||||||
|
|
||||||
|
if (!HEX_STRING_RE.test(digest)) {
|
||||||
|
throw new Error(`Invalid digest: ${digest}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
subjects.push({
|
||||||
|
name,
|
||||||
|
digest: { [digestAlgorithm(digest)]: digest }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return subjects
|
||||||
|
}
|
||||||
|
|
||||||
// Calculates the digest of a file using the specified algorithm. The file is
|
// Calculates the digest of a file using the specified algorithm. The file is
|
||||||
// streamed into the digest function to avoid loading the entire file into
|
// streamed into the digest function to avoid loading the entire file into
|
||||||
// memory. The returned digest is a hex string.
|
// memory. The returned digest is a hex string.
|
||||||
@@ -135,7 +214,7 @@ const digestFile = async (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseList = (input: string): string[] => {
|
const parseSubjectPathList = (input: string): string[] => {
|
||||||
const res: string[] = []
|
const res: string[] = []
|
||||||
|
|
||||||
const records: string[][] = parse(input, {
|
const records: string[][] = parse(input, {
|
||||||
@@ -151,3 +230,14 @@ const parseList = (input: string): string[] => {
|
|||||||
|
|
||||||
return res.filter(item => item).map(pat => pat.trim())
|
return res.filter(item => item).map(pat => pat.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const digestAlgorithm = (digest: string): string => {
|
||||||
|
switch (digest.length) {
|
||||||
|
case 64:
|
||||||
|
return 'sha256'
|
||||||
|
case 128:
|
||||||
|
return 'sha512'
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown digest algorithm: ${digest}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user