mirror of
https://github.com/go-task/task.git
synced 2025-05-31 23:19:42 +02:00
feat: checksum pinning
This commit is contained in:
parent
d47bad9071
commit
3daaf92d4c
@ -26,6 +26,7 @@ const (
|
||||
CodeTaskfileNetworkTimeout
|
||||
CodeTaskfileInvalid
|
||||
CodeTaskfileCycle
|
||||
CodeTaskfileDoesNotMatchChecksum
|
||||
)
|
||||
|
||||
// Task related exit codes
|
||||
|
@ -187,3 +187,24 @@ func (err TaskfileCycleError) Error() string {
|
||||
func (err TaskfileCycleError) Code() int {
|
||||
return CodeTaskfileCycle
|
||||
}
|
||||
|
||||
// TaskfileDoesNotMatchChecksum is returned when a Taskfile's checksum does not
|
||||
// match the one pinned in the parent Taskfile.
|
||||
type TaskfileDoesNotMatchChecksum struct {
|
||||
URI string
|
||||
ExpectedChecksum string
|
||||
ActualChecksum string
|
||||
}
|
||||
|
||||
func (err *TaskfileDoesNotMatchChecksum) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"task: The checksum of the Taskfile at %q does not match!\ngot: %q\nwant: %q",
|
||||
err.URI,
|
||||
err.ActualChecksum,
|
||||
err.ExpectedChecksum,
|
||||
)
|
||||
}
|
||||
|
||||
func (err *TaskfileDoesNotMatchChecksum) Code() int {
|
||||
return CodeTaskfileDoesNotMatchChecksum
|
||||
}
|
||||
|
@ -958,3 +958,23 @@ func TestFuzzyModel(t *testing.T) {
|
||||
WithTask("install"),
|
||||
)
|
||||
}
|
||||
|
||||
func TestIncludeChecksum(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("correct"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/includes_checksum/correct"),
|
||||
),
|
||||
)
|
||||
|
||||
NewExecutorTest(t,
|
||||
WithName("incorrect"),
|
||||
WithExecutorOptions(
|
||||
task.WithDir("testdata/includes_checksum/incorrect"),
|
||||
),
|
||||
WithSetupError(),
|
||||
WithPostProcessFn(PPRemoveAbsolutePaths),
|
||||
)
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ type (
|
||||
AdvancedImport bool
|
||||
Vars *Vars
|
||||
Flatten bool
|
||||
Checksum string
|
||||
}
|
||||
// Includes is an ordered map of namespaces to includes.
|
||||
Includes struct {
|
||||
@ -165,6 +166,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
|
||||
Aliases []string
|
||||
Excludes []string
|
||||
Vars *Vars
|
||||
Checksum string
|
||||
}
|
||||
if err := node.Decode(&includedTaskfile); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
@ -178,6 +180,7 @@ func (include *Include) UnmarshalYAML(node *yaml.Node) error {
|
||||
include.AdvancedImport = true
|
||||
include.Vars = includedTaskfile.Vars
|
||||
include.Flatten = includedTaskfile.Flatten
|
||||
include.Checksum = includedTaskfile.Checksum
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -200,5 +203,7 @@ func (include *Include) DeepCopy() *Include {
|
||||
AdvancedImport: include.AdvancedImport,
|
||||
Vars: include.Vars.DeepCopy(),
|
||||
Flatten: include.Flatten,
|
||||
Aliases: deepcopy.Slice(include.Aliases),
|
||||
Checksum: include.Checksum,
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,8 @@ type Node interface {
|
||||
Parent() Node
|
||||
Location() string
|
||||
Dir() string
|
||||
Checksum() string
|
||||
Verify(checksum string) bool
|
||||
ResolveEntrypoint(entrypoint string) (string, error)
|
||||
ResolveDir(dir string) (string, error)
|
||||
}
|
||||
|
@ -7,8 +7,9 @@ type (
|
||||
// designed to be embedded in other node types so that this boilerplate code
|
||||
// does not need to be repeated.
|
||||
baseNode struct {
|
||||
parent Node
|
||||
dir string
|
||||
parent Node
|
||||
dir string
|
||||
checksum string
|
||||
}
|
||||
)
|
||||
|
||||
@ -32,6 +33,12 @@ func WithParent(parent Node) NodeOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithChecksum(checksum string) NodeOption {
|
||||
return func(node *baseNode) {
|
||||
node.checksum = checksum
|
||||
}
|
||||
}
|
||||
|
||||
func (node *baseNode) Parent() Node {
|
||||
return node.parent
|
||||
}
|
||||
@ -39,3 +46,11 @@ func (node *baseNode) Parent() Node {
|
||||
func (node *baseNode) Dir() string {
|
||||
return node.dir
|
||||
}
|
||||
|
||||
func (node *baseNode) Checksum() string {
|
||||
return node.checksum
|
||||
}
|
||||
|
||||
func (node *baseNode) Verify(checksum string) bool {
|
||||
return node.checksum == "" || node.checksum == checksum
|
||||
}
|
||||
|
@ -250,6 +250,7 @@ func (r *Reader) include(ctx context.Context, node Node) error {
|
||||
AdvancedImport: include.AdvancedImport,
|
||||
Excludes: include.Excludes,
|
||||
Vars: include.Vars,
|
||||
Checksum: include.Checksum,
|
||||
}
|
||||
if err := cache.Err(); err != nil {
|
||||
return err
|
||||
@ -267,6 +268,7 @@ func (r *Reader) include(ctx context.Context, node Node) error {
|
||||
|
||||
includeNode, err := NewNode(entrypoint, include.Dir, r.insecure,
|
||||
WithParent(node),
|
||||
WithChecksum(include.Checksum),
|
||||
)
|
||||
if err != nil {
|
||||
if include.Optional {
|
||||
@ -362,7 +364,24 @@ func (r *Reader) readNodeContent(ctx context.Context, node Node) ([]byte, error)
|
||||
if node, isRemote := node.(RemoteNode); isRemote {
|
||||
return r.readRemoteNodeContent(ctx, node)
|
||||
}
|
||||
return node.Read()
|
||||
|
||||
// Read the Taskfile
|
||||
b, err := node.Read()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the given checksum doesn't match the sum pinned in the Taskfile
|
||||
checksum := checksum(b)
|
||||
if !node.Verify(checksum) {
|
||||
return nil, &errors.TaskfileDoesNotMatchChecksum{
|
||||
URI: node.Location(),
|
||||
ExpectedChecksum: node.Checksum(),
|
||||
ActualChecksum: checksum,
|
||||
}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]byte, error) {
|
||||
@ -427,17 +446,29 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]
|
||||
}
|
||||
|
||||
r.debugf("found remote file at %q\n", node.Location())
|
||||
checksum := checksum(downloadedBytes)
|
||||
prompt := cache.ChecksumPrompt(checksum)
|
||||
|
||||
// Prompt the user if required
|
||||
if prompt != "" {
|
||||
if err := func() error {
|
||||
r.promptMutex.Lock()
|
||||
defer r.promptMutex.Unlock()
|
||||
return r.promptf(prompt, node.Location())
|
||||
}(); err != nil {
|
||||
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
|
||||
// If the given checksum doesn't match the sum pinned in the Taskfile
|
||||
checksum := checksum(downloadedBytes)
|
||||
if !node.Verify(checksum) {
|
||||
return nil, &errors.TaskfileDoesNotMatchChecksum{
|
||||
URI: node.Location(),
|
||||
ExpectedChecksum: node.Checksum(),
|
||||
ActualChecksum: checksum,
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no manual checksum pin, run the automatic checks
|
||||
if node.Checksum() == "" {
|
||||
// Prompt the user if required
|
||||
prompt := cache.ChecksumPrompt(checksum)
|
||||
if prompt != "" {
|
||||
if err := func() error {
|
||||
r.promptMutex.Lock()
|
||||
defer r.promptMutex.Unlock()
|
||||
return r.promptf(prompt, node.Location())
|
||||
}(); err != nil {
|
||||
return nil, &errors.TaskfileNotTrustedError{URI: node.Location()}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
12
testdata/includes_checksum/correct/Taskfile.yml
vendored
Normal file
12
testdata/includes_checksum/correct/Taskfile.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
included:
|
||||
taskfile: ../included.yml
|
||||
internal: true
|
||||
checksum: c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- task: included:default
|
2
testdata/includes_checksum/correct/testdata/TestIncludeChecksum-correct.golden
vendored
Normal file
2
testdata/includes_checksum/correct/testdata/TestIncludeChecksum-correct.golden
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
task: [included:default] echo "Hello, World!"
|
||||
Hello, World!
|
12
testdata/includes_checksum/correct_remote/Taskfile.yml
vendored
Normal file
12
testdata/includes_checksum/correct_remote/Taskfile.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
included:
|
||||
taskfile: https://taskfile.dev
|
||||
internal: true
|
||||
checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- task: included:default
|
6
testdata/includes_checksum/included.yml
vendored
Normal file
6
testdata/includes_checksum/included.yml
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- echo "Hello, World!"
|
12
testdata/includes_checksum/incorrect/Taskfile.yml
vendored
Normal file
12
testdata/includes_checksum/incorrect/Taskfile.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
included:
|
||||
taskfile: ../included.yml
|
||||
internal: true
|
||||
checksum: foo
|
||||
|
||||
tasks:
|
||||
default:
|
||||
cmds:
|
||||
- task: included:default
|
3
testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect-err-setup.golden
vendored
Normal file
3
testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect-err-setup.golden
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
task: The checksum of the Taskfile at "/testdata/includes_checksum/included.yml" does not match!
|
||||
got: "c97f39eb96fe3fa5fe2a610d244b8449897b06f0c93821484af02e0999781bf5"
|
||||
want: "foo"
|
0
testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect.golden
vendored
Normal file
0
testdata/includes_checksum/incorrect/testdata/TestIncludeChecksum-incorrect.golden
vendored
Normal file
@ -182,9 +182,11 @@ includes:
|
||||
|
||||
## Security
|
||||
|
||||
### Automatic checksums
|
||||
|
||||
Running commands from sources that you do not control is always a potential
|
||||
security risk. For this reason, we have added some checks when using remote
|
||||
Taskfiles:
|
||||
security risk. For this reason, we have added some automatic checks when using
|
||||
remote Taskfiles:
|
||||
|
||||
1. When running a task from a remote Taskfile for the first time, Task will
|
||||
print a warning to the console asking you to check that you are sure that you
|
||||
@ -209,6 +211,38 @@ flag. Before enabling this flag, you should:
|
||||
containing a commit hash) to prevent Task from automatically accepting a
|
||||
prompt that says a remote Taskfile has changed.
|
||||
|
||||
### Manual checksum pinning
|
||||
|
||||
Alternatively, if you expect the contents of your remote files to be a constant
|
||||
value, you can pin the checksum of the included file instead:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
|
||||
includes:
|
||||
included:
|
||||
taskfile: https://taskfile.dev
|
||||
checksum: c153e97e0b3a998a7ed2e61064c6ddaddd0de0c525feefd6bba8569827d8efe9
|
||||
```
|
||||
|
||||
This will disable the automatic checksum prompts discussed above. However, if
|
||||
the checksums do not match, Task will exit immediately with an error. When
|
||||
setting this up for the first time, you may not know the correct value of the
|
||||
checksum. There are a couple of ways you can obtain this:
|
||||
|
||||
1. Add the include normally without the `checksum` key. The first time you run
|
||||
the included Taskfile, a `.task/remote` temporary directory is created. Find
|
||||
the correct set of files for your included Taskfile and open the file that
|
||||
ends with `.checksum`. You can copy the contents of this file and paste it
|
||||
into the `checksum` key of your include. This method is safest as it allows
|
||||
you to inspect the downloaded Taskfile before you pin it.
|
||||
2. Alternatively, add the include with a temporary random value in the
|
||||
`checksum` key. When you try to run the Taskfile, you will get an error that
|
||||
will report the incorrect expected checksum and the actual checksum. You can
|
||||
copy the actual checksum and replace your temporary random value.
|
||||
|
||||
### TLS
|
||||
|
||||
Task currently supports both `http` and `https` URLs. However, the `http`
|
||||
requests will not execute by default unless you run the task with the
|
||||
`--insecure` flag. This is to protect you from accidentally running a remote
|
||||
|
@ -34,6 +34,7 @@ toc_max_heading_level: 5
|
||||
| `internal` | `bool` | `false` | Stops any task in the included Taskfile from being callable on the command line. These commands will also be omitted from the output when used with `--list`. |
|
||||
| `aliases` | `[]string` | | Alternative names for the namespace of the included Taskfile. |
|
||||
| `vars` | `map[string]Variable` | | A set of variables to apply to the included Taskfile. |
|
||||
| `checksum` | `string` | | The checksum of the file you expect to include. If the checksum does not match, the file will not be included. |
|
||||
|
||||
:::info
|
||||
|
||||
|
@ -684,6 +684,10 @@
|
||||
"vars": {
|
||||
"description": "A set of variables to apply to the included Taskfile.",
|
||||
"$ref": "#/definitions/vars"
|
||||
},
|
||||
"checksum": {
|
||||
"description": "The checksum of the file you expect to include. If the checksum does not match, the file will not be included.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user